Have an idea?

Visit Sawtooth Software Feedback to share your ideas on how we can improve our products.

Gabor–Granger method

I have a client asking about programming a study with a random laddering technique (Gabor Granger).  I'm not familiar with this but have looked up the basic method.  Does anyone have experience on the best way/method to program this logic?  

From what I've seen, you're looking at 5 price points and asking one randomly on a 5 point scale.  If in the top 2 box move to the next highest amount until they no longer answer in the top 2 box then stop.  If the first price is not in the top 2 box, ask the next lowest price until they give a response in the top 2 box or until all lower prices have been asked.
asked Feb 8 by Jay Rutherford Gold (26,135 points)

1 Answer

0 votes
I've just finished my Gabor-Granger question.  It was a bit of a unique challenge from a design perspective, so I would be interesting in hearing any thoughts on user experience and clarity and all that fun stuff.

To build it, start with a free format question.  The free format requires a hidden, text variable named "_Price."

Somewhere in the question texts, place something like this to display the current price to the respondent:

How likely are you to buy this product for <b><span class="gaborGrangerPrice"></span></b>?


Optionally place something like this elsewhere in the question texts to inform respondents that the question is complete:

<span class="gaborGrangerFinished"><i>Your responses have been recorded.  Please continue with the survey.</i></span>


Finally, set the question HTML to this:

<input name="[% QuestionName() %]_Price" id="[% QuestionName() %]_Price" type="hidden" value=""/>
<div class="gaborGrangerContainer"></div>

<style>
.gaborGrangerFinished {
    display: none;
}

.gaborGrangerButton {
    display: block;
    width: 200px;
    margin: 10px 0px;
    padding: 5px;
    cursor: pointer;
}

.gaborGrangerButton:hover {
    background-color: #CDCDCD;
}
</style>

<script>
$(document).ready(function(){
    var prices = ['$1', '$2', '$3', '$4', '$5'];
    var startPosition = 'middle'; // the position of the first price to show, or 'middle' to automatically select middle price; 'random' to select random starting price
    // Gabor-Granger experiments are typically performed with either two acceptables and three unacceptables, or one acceptable and one unaccpetable
    var acceptableResponses = ['Extremely likely', 'Very likely'];
    var unacceptableResponses = ['Somewhat likely', 'Not very likely', 'Not at all likely'];
    var noneResponse = 'NONE';
    
    // initialize price
    var gaborPosition;
    switch (startPosition.toString().toLowerCase()) {
        case 'middle':
            gaborPosition = Math.ceil(prices.length / 2);
            break;
        case 'random':
            gaborPosition = Math.ceil(Math.random() * prices.length)
            break;
        default:
            gaborPosition = Number(startPosition);
            break;
    }
    $('#[% QuestionName() %]_div .gaborGrangerPrice').text(prices[gaborPosition - 1]);
    
    // create buttons
    acceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton acceptable">' + response + '</button>');
    });
    unacceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton unacceptable">' + response + '</button>');
    });
    
    // click events
    var gaborPreviousPosition;
    $('#[% QuestionName() %]_div .gaborGrangerButton').click(function(){
        var acceptable = $(this).is('.acceptable');
        if (gaborPosition == 1 && !acceptable) {
            finishGaborGranger('[% QuestionName() %]', noneResponse);
        }
        else if ((gaborPosition == prices.length && acceptable) ||
            (acceptable && gaborPreviousPosition && gaborPosition < gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
        else if ((gaborPosition == prices.length && !acceptable && gaborPreviousPosition) ||
            (gaborPosition == 1 && acceptable && gaborPreviousPosition) ||
            (!acceptable && gaborPreviousPosition && gaborPosition > gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPreviousPosition - 1]);
        }
        else {
            gaborPreviousPosition = gaborPosition;
            gaborPosition += acceptable ? 1 : -1;
            updateGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
    });
})

function updateGaborGranger(question, price) {
    var qdiv = $('#' + question + '_div');
    $(qdiv).fadeOut(500, function(){
        $(qdiv).find('.gaborGrangerPrice').text(price);
        var bgColor = $(qdiv).css('background-color');
        $(qdiv).css('background-color', 'lightcyan');
        $(qdiv).fadeIn(1000, function(){
            $(qdiv).css('background-color', bgColor);
        });
    });
}

function finishGaborGranger(question, price) {
    $('#' + question + '_Price').val(price);
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.gaborGrangerButton').prop('disabled', true).css('cursor', 'default');
    $(qdiv).find('.gaborGrangerFinished').show();
}
</script>


Line 24 should be updated with the relevant prices, from lowest to highest.

Line 25 can be updated to set which price a respondent starts on.

Line 27 and 28 can be updated with the acceptable and unacceptable response options.  From what I can tell, most Gabor-Granger experiments have (A) two acceptables and three unacceptables, or (B) an acceptable "Yes" and an unacceptable "No."

Line 29 can be updated with the value to be recorded if a respondent says that the lowest price is unacceptable.
answered Feb 9 by Zachary Platinum Sawtooth Software, Inc. (86,625 points)
That's great Zach.  I'll take a deeper dive into your code on Monday and give it a test drive.  I actually just got finished writing up a test program of my own just using a series of questions with a lot of list building along with Pass-In variables to see if I could get something to work.  Nowhere near as elegant to what you have and wouldn't be too friendly if it got more complex with more items, but it would work in a pinch.  Excited to look over what you have more and I appreciate you taking the time to put something together.
I like what you put together.  I have a few questions/comments regarding this.
- I see how it flashes and moves to the next price point, however, at the last item it just goes gray and isn't intuitive that you should click next to move on.  Can this automatically move forward when completed, or get an instruction?
- Is it possible to store each response in a different variable, should the individual data be wanted for any reason?
- Not being familiar with this method, I have to wonder, would they want the value set to the highest one with the highest rating if none were in the top two box?  As you have it, it defaults to 'none' or whatever value you want.  When I was working through this I had set a value to the highest even if that wasn't in that acceptable (top 2 box) range.  Did you find your method to be the standard for this analysis?  
-
From what I've read it would seem there should always be a price point generated (even if not in the top 2 box, then whatever their highest threshold is).
Clarity for when the question ends was a concern for me as well.  Currently, my simple fix is to show the extra text (within "<span class="gaborGrangerFinished">") when the question is finished.  If this is insufficient, we may need to consider other options.  Automatically submitting the page is a possibility, but would limit survey creators from putting this question on the same page as other questions.

What prices a respondent saw and whether they answered acceptable or unacceptable can be deduced from their final price point (as long as you don't start at a random price).  But I could add a second variable that records all of the respondent's choices if there is statistical relevance as to which acceptable answer the respondent selected and which unacceptable answer the respondent selected.

The resources I have briefly looked through have not offered specifics regarding what should happen if a respondent never marks a price as acceptable.  If you have sources with more information on Gabor-Granger implementation, I would be interested in reading them.
I think using the text within the span tags would work to let them know the exercise is complete.  Thanks.  You are correct and it looks like it's suggested to code any that don't give a top 2 box rating as a 0 or to just remove from the analysis.  I'll have to discuss with this client further if they proceed with this as to what they want for deliverables. Just the final variable score or the data from the steps as well.  It's probably just the final score, but the steps data is good to check/validate the result in the data.
I have made adjustments to record exact responses.

Add two more hidden, text variables "_StartPrice" and "_Responses," then change the HTML to this:

<input name="[% QuestionName() %]_Price" id="[% QuestionName() %]_Price" type="hidden" value=""/>
<input name="[% QuestionName() %]_StartPrice" id="[% QuestionName() %]_StartPrice" type="hidden" value=""/>
<input name="[% QuestionName() %]_Responses" id="[% QuestionName() %]_Responses" type="hidden" value=""/>

<div class="gaborGrangerContainer"></div>

<style>
.gaborGrangerFinished {
    display: none;
}

.gaborGrangerButton {
    display: block;
    width: 200px;
    margin: 10px 0px;
    padding: 5px;
    cursor: pointer;
}

.gaborGrangerButton:hover {
    background-color: #CDCDCD;
}
</style>

<script>
$(document).ready(function(){
    var prices = ['$1', '$2', '$3', '$4', '$5'];
    var startPosition = 'middle'; // the position of the first price to show, or 'middle' to automatically select middle price, or 'random' to select random starting price
    // Gabor-Granger experiments are typically performed with either two acceptables and three unacceptables, or one acceptable and one unaccpetable
    var acceptableResponses = ['Extremely likely', 'Very likely'];
    var unacceptableResponses = ['Somewhat likely', 'Not very likely', 'Not at all likely'];
    var noneResponse = 'NONE';
    
    // initialize price
    var gaborPosition;
    switch (startPosition.toString().toLowerCase()) {
        case 'middle':
            gaborPosition = Math.ceil(prices.length / 2);
            break;
        case 'random':
            gaborPosition = Math.ceil(Math.random() * prices.length)
            break;
        default:
            gaborPosition = Number(startPosition);
            break;
    }
    $('#[% QuestionName() %]_div .gaborGrangerPrice').text(prices[gaborPosition - 1]);
    
    // create buttons
    var responseCounter = 1;
    acceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton acceptable" data-gabor="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
    unacceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton unacceptable" data-gabor="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
    
    // click events
    var gaborPreviousPosition;
    var firstClick = true;
    var responsesSep = '';
    $('#[% QuestionName() %]_div .gaborGrangerButton').click(function(){
        if (firstClick) {
            $('#[% QuestionName() %]_Price').val('');
            $('#[% QuestionName() %]_StartPrice').val(prices[gaborPosition - 1]);
            $('#[% QuestionName() %]_Responses').val('');
            firstClick = false;
        }
        
        $('#[% QuestionName() %]_Responses').val($('#[% QuestionName() %]_Responses').val() + responsesSep + $(this).data('gabor'));
        responsesSep = ',';
    
        var acceptable = $(this).is('.acceptable');
        if (gaborPosition == 1 && !acceptable) {
            finishGaborGranger('[% QuestionName() %]', noneResponse);
        }
        else if ((gaborPosition == prices.length && acceptable) ||
            (acceptable && gaborPreviousPosition && gaborPosition < gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
        else if ((gaborPosition == prices.length && !acceptable && gaborPreviousPosition) ||
            (gaborPosition == 1 && acceptable && gaborPreviousPosition) ||
            (!acceptable && gaborPreviousPosition && gaborPosition > gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPreviousPosition - 1]);
        }
        else {
            gaborPreviousPosition = gaborPosition;
            gaborPosition += acceptable ? 1 : -1;
            updateGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
    });
})

function updateGaborGranger(question, price) {
    var qdiv = $('#' + question + '_div');
    $(qdiv).fadeOut(500, function(){
        $(qdiv).find('.gaborGrangerPrice').text(price);
        var bgColor = $(qdiv).css('background-color');
        $(qdiv).css('background-color', 'lightcyan');
        $(qdiv).fadeIn(1000, function(){
            $(qdiv).css('background-color', bgColor);
        });
    });
}

function finishGaborGranger(question, price) {
    $('#' + question + '_Price').val(price);
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.gaborGrangerButton').prop('disabled', true).css('cursor', 'default');
    $(qdiv).find('.gaborGrangerFinished').show();
}
</script>


In addition, here's the custom JS verification to require a response:

if (!SSI_GetValue('[% QuestionName() %]_Price')) {
    strErrorMessage = 'A response is required.';
}
Fantastic work as always!!!  Works nicely and now should they want the response data, it's available.
...