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, 2018 by Jay Rutherford Gold (38,005 points)
Hi Guys! Great, this code helped me a lot! I am seeking for e way to hide the Next button untill the Gabor-Granger is finished. Is there someone with experience doing this? Thanks a lot in advance!

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, 2018 by Zachary Platinum Sawtooth Software, Inc. (156,150 points)
I found the issue. They were not being hidden. The problem I have now is that those variables are not recording anything. I see values in the Price, StartPrice and Responses, but not the individual price variables. What is missing?
Is there a reason you need to stitch together the new code with the old one?  Why not just use the new code from Jay's link?
I didn't know that was "newer" than this one and  couldn't detect the differences except for the variable naming. So now I used the "new," but I only see the recording of the values for the answers for the prices seen between the first and the last. I need to record of the scale values for all the prices seen. How do I do that? I had to add Price5 and Price6 variables since I have 6 prices, but I don't seem to be able to record anything for Price5 and Price6, even if shown
Here is my code:
<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=""/>
<input name="[% QuestionName() %]_Response1" id="[% QuestionName() %]_Response1" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response2" id="[% QuestionName() %]_Response2" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response3" id="[% QuestionName() %]_Response3" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response4" id="[% QuestionName() %]_Response4" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response3" id="[% QuestionName() %]_Response5" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response4" id="[% QuestionName() %]_Response6" type="hidden" value=""/>
 
<div class="priceLadderingContainer"></div>
 
<style>
#[% QuestionName() %]_div {
    transition: background 0.5s linear;
}
 
.priceLadderingFinished {
    display: none;
}
 
.priceLadderingButton
{
    display: block;
    background-color: white;
    border: 1px solid green;
    width: 250px;
    margin: 20px 0px;
    padding: 10px;
    font-family: "Open Sans";
    font-size: medium;
    font-weight: 400;
    cursor: pointer;
}
}
 
.priceLadderingButton:hover {
    background-color: #CDCDCD;
}
</style>
 
<script>
$(document).ready(function(){
    var prices = ['$100', '$125', '$150', '$175', '$200', '$225'];
    var startPosition = 'random'; // the position of the first price to show, or 'middle' to automatically select middle price, or 'random' to select random starting price
    // Price laddering experiments are typically performed with either two acceptables and three unacceptables, or one acceptable and one unaccpetable (yes and no)
    var acceptableResponses = ['Extremely likely', 'Very likely'];
    var unacceptableResponses = ['Somewhat likely', 'Not very likely', 'Not at all likely'];
    var noneResponse = 'NONE';
     
    // initialize price
    var priceLadderingPosition;
    switch (startPosition.toString().toLowerCase()) {
        case 'middle':
            priceLadderingPosition = Math.ceil(prices.length / 2);
            break;
        case 'random':
            priceLadderingPosition = Math.ceil(Math.random() * prices.length)
            break;
        default:
            priceLadderingPosition = Number(startPosition);
            break;
    }
    $('#[% QuestionName() %]_div .priceLadderingPrice').text(prices[priceLadderingPosition - 1]);
     
    // reset individual responses
    $('#[% QuestionName() %]_Response1').val('');
    $('#[% QuestionName() %]_Response2').val('');
    $('#[% QuestionName() %]_Response3').val('');
    $('#[% QuestionName() %]_Response4').val('');
    $('#[% QuestionName() %]_Response5').val('');
    $('#[% QuestionName() %]_Response6').val('');
     
    // create buttons
    var responseCounter = 1;
    acceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .priceLadderingContainer').append('<button type="button" class="priceLadderingButton acceptable" data-priceladdering="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
    unacceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .priceLadderingContainer').append('<button type="button" class="priceLadderingButton unacceptable" data-priceladdering="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
     
    // click events
    var priceLadderingPreviousPosition;
    var firstClick = true;
    var responsesSep = '';
    $('#[% QuestionName() %]_div .priceLadderingButton').click(function(){
        if (firstClick) {
            $('#[% QuestionName() %]_Price').val('');
            $('#[% QuestionName() %]_StartPrice').val(prices[priceLadderingPosition - 1]);
            $('#[% QuestionName() %]_Responses').val('');
            firstClick = false;
        }
         
        var resp = $(this).data('priceladdering');
        $('#[% QuestionName() %]_Responses').val($('#[% QuestionName() %]_Responses').val() + responsesSep + resp);
        responsesSep = ',';
         
        $('#[% QuestionName() %]_Response' + priceLadderingPosition).val(resp);
     
        var acceptable = $(this).is('.acceptable');
        if (priceLadderingPosition == 1 && !acceptable) {
            finishPriceLaddering('[% QuestionName() %]', noneResponse);
        }
        else if ((priceLadderingPosition == prices.length && acceptable) ||
            (acceptable && priceLadderingPreviousPosition && priceLadderingPosition < priceLadderingPreviousPosition)) {
            finishPriceLaddering('[% QuestionName() %]', prices[priceLadderingPosition - 1]);
        }
        else if ((priceLadderingPosition == prices.length && !acceptable && priceLadderingPreviousPosition) ||
            (priceLadderingPosition == 1 && acceptable && priceLadderingPreviousPosition) ||
            (!acceptable && priceLadderingPreviousPosition && priceLadderingPosition > priceLadderingPreviousPosition)) {
            finishPriceLaddering('[% QuestionName() %]', prices[priceLadderingPreviousPosition - 1]);
        }
        else {
            priceLadderingPreviousPosition = priceLadderingPosition;
            priceLadderingPosition += acceptable ? 1 : -1;
            updatePriceLaddering('[% QuestionName() %]', prices[priceLadderingPosition - 1]);
        }
    });
})
 
function updatePriceLaddering(question, price) {
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.priceLadderingPrice').text(price);
    $(qdiv).css({
        'transition': 'background-color 0.5s ease-in-out',
        'background-color': '#e6ffff'
    });
    var interval = setInterval(function(){
        $(qdiv).css({
            'transition': 'background-color 1s ease-in-out',
            'background-color': 'transparent'
        });
        clearInterval(interval);
    }, 500);
}
 
function finishPriceLaddering(question, price) {
    $('#' + question + '_Price').val(price);
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.priceLadderingButton').prop('disabled', true).css('cursor', 'default');
    $(qdiv).find('.priceLadderingFinished').show();
}
</script>
You missed something in lines 8 and 9.
Good catch. Thanks!
...