Pular para o conteúdo principal
Base de Conhecimento da FocusVision

Shuffling Answer Options in Groups

1:  What is Group Shuffling?

Shuffling means to show the answer options in a question in random order for each respondent. It is a common task when building surveys. Shuffling in groups can mean a couple of things:

  • Shuffling groups:  You have a set of rows that need to be randomized and a subset of these rows should remain grouped together.
  • Shuffling groups with group order:  You have a set of rows that need to be randomized and a subset of these rows should remain grouped together in a specific order.
  • Shuffling using shuffleBy:  You have a set of rows that need to be randomized in the same order as a previous question's rows, but the number of rows do not match.

Please review the Advanced Shuffling document if you're not familiar with the shuffling syntax and continue reading when you're ready to see solutions revolving around more advanced shuffling tasks.

The following sections are used to shuffle rows. The same approach can be taken to shuffle column elements, but you must change the code to use cols instead of rows.

2:  Shuffling Groups

Group shuffling can be accomplished in the survey builder using groups. Click here to learn more about the Group Tag.

For now, let's look at an alternative approach that's dynamic and relatively easy.

Context

You have a set of rows that need to be randomized and a subset of these rows should remain grouped together.

Example

In this example, there are 8 rows and we need rows 5, 6 and 7 to be positioned next to each other.

<radio label="Q1" shuffle="rows">
    <title>Standard radio question.</title>
    <comment>Please choose one.</comment>
    <row label="r1">r1</row>
    <row label="r2">r2</row>
    <row label="r3">r3</row>
    <row label="r4">r4</row>
    <row label="r5">r5</row>
    <row label="r6">r6</row>
    <row label="r7">r7</row>
    <row label="r8">r8</row>
</radio>

Without any modifications to the question above, the rows will shuffle for each respondent and rows 5, 6 and 7 will not always remain grouped together. The result is illustrated below.

demos_shuffle_base.png

Rows 5, 6 and 7 are not presented next to each other. The correct and intended behavior is for our question to look like the layout below:

examples_shuffle_subsetshuffle.png

Notice how rows 5, 6 and 7 are now grouped together. It wasn't specified to show these rows in the same order, just that they should be positioned in the table next to each other. If you need the rows to be in a specific order, click here.

Solution ( Pseudocode )

To achieve this result, we want to manually specify the row order for the question. The system will handle the randomization and we will need to specify the code to group the rows together. Below is a description of the function we'll need to create to make this happen:

Step 1:  Create one variable to store an index value (used in step #3)

Step 2:  Iterate through a copy of the question's shuffle order

Step 3:  The first time we reach a row that is in the set of rows to be grouped together, store the row's index position relative to the question's shuffle order into the variable (created in step #1)

Step 4:  As we continue iterating through the shuffle order, each time we reach a row that is in this set of rows to be grouped together, increment our index variable (noted in step #3) and move this row into this new position

Step 5:  The final step is to set the question's shuffle order to the copy that we just modified

Solution ( In Practice )

We will accomplish this task using <exec> blocks and Python code. Let's begin by creating a function called group_rows to perform the procedures written out in steps 1 - 5 above.

This function will accept two arguments: the question we are performing the manual shuffle on and a list of row labels relative to the rows that should be grouped together.

def group_rows( question, grouped_rows ):

When the time is right, we'll call this function from our survey like this:

exec="group_rows( Q1, ['r5', 'r6', 'r7'] )"

Let's finish writing the function based on steps 1 - 5 listed above.

def group_rows( question, grouped_rows ):
    # Create one variable to store an index value
    first_item_index = None

    # Iterate through a copy of the question's shuffle order
    shuffle_order = [row.index for row in question.rows.order]

    for index, row in enumerate( shuffle_order ):
        # The first time we reach a row that is in the set of rows
        # to be grouped together, store the row's index position
        # relative to the question's shuffle order into the variable
        if question.rows[row].label in grouped_rows:
            if first_item_index == None:
                first_item_index = index
            else:
                # As we continue iterating through the shuffle order
                # each time we reach a row that is in the set of
                # rows to be grouped together, increment our index
                # value and move this row into the new position
                first_item_index += 1
                shuffle_order.insert( first_item_index, shuffle_order.pop(index) )

    # The final step is to set the question's shuffle order to the copy that
    # we just modified
    question.rows.order = shuffle_order

That completes our function and fulfills the requirements specified for this task. As shown below, we can apply this function to our survey by adding it (without the comments) to an <exec> block that is called once when the survey is loaded. We can then call this function at our question using the <exec> attribute.

<exec when="init">
def group_rows( question, grouped_rows ):
    first_item_index = None
    shuffle_order = [row.index for row in question.rows.order]

    for index, row in enumerate( shuffle_order ):
        if question.rows[row].label in grouped_rows:
            if first_item_index == None:
                first_item_index = index
            else:
                first_item_index += 1
                shuffle_order.insert( first_item_index, shuffle_order.pop(index) )

    question.rows.order = shuffle_order
</exec>

<radio label="Q1" shuffle="rows" exec="group_rows(Q1, ['r5', 'r6', 'r7'])">
    <title>Standard radio question</title>
    <comment>Please choose one.</comment>
    <row label="r1">r1</row>
    <row label="r2">r2</row>
    <row label="r3">r3</row>
    <row label="r4">r4</row>
    <row label="r5">r5</row>
    <row label="r6">r6</row>
    <row label="r7">r7</row>
    <row label="r8">r8</row>
</radio>

Result

We can assert that our function is working correctly by verifying that each time the question is loaded, rows 5, 6 and 7 are shuffled together.

Refresh the demo page to verify the correct result has been achieved.

Real World Application

This function would apply well to the question below given the following programmer note:

Programmer Note: Randomize rows. Keep Google products grouped together.

<checkbox label="Q4" shuffle="rows" exec="group_rows(Q4, ['r3', 'r4', 'r5'])">
    <title>Please select the services you currently use.</title>
    <comment>Select all that apply.</comment>
    <row label="r1">Bing Search</row>
    <row label="r2">DuckDuckGo</row>
    <row label="r3">Google Search</row>
    <row label="r4">Google Mail (GMail)</row>
    <row label="r5">Google Drive</row>
    <row label="r6">Yahoo Search</row>
    <row label="r7">Dropbox</row>
</radio>

This function can be stacked if we needed to group more than one set of rows. For example, if we also wanted to group "Bing Search" with "Yahoo Search" in the example above, we would just need to modify the question's exec attribute to call the function again for this additional set.

<checkbox label="Q4" shuffle="rows" exec="group_rows(Q4, ['r3', 'r4', 'r5']); group_rows(Q4, ['r1', 'r6']);">

Conclusion & Thoughts

  • More information about the enumerate function used can be found here.

3:  Shuffling Groups with Group Order

Context

You have a set of rows that need to be randomized and a subset of these rows should remain grouped together in a specific order.

Example

In this example, there are 8 rows and we need rows 5, 6 and 7 to be positioned next to each other in the specified order. For the sake of this example, we'll arrange the rows in ascending order.

<radio label="Q1" shuffle="rows">
    <title>Standard radio question.</title>
    <comment>Please choose one.</comment>
    <row label="r1">r1</row>
    <row label="r2">r2</row>
    <row label="r3">r3</row>
    <row label="r4">r4</row>
    <row label="r5">r5</row>
    <row label="r6">r6</row>
    <row label="r7">r7</row>
    <row label="r8">r8</row>
</radio>

Without any modifications to the question above, the rows will shuffle for each respondent and rows 5, 6 and 7 will not remain grouped together. The result is illustrated below.

demos_shuffle_base.png

Rows 5, 6 and 7 are not presented next to each other. The correct and intended behavior is for our question to look like the layout below:

examples_shuffle_order.png

Notice how rows 5, 6 and 7 are now grouped together and in the specified order.

Solution ( Pseudocode )

To achieve this result, we want to manually specify the row order for the question. The system will handle the randomization and we will need to specify the code to group the rows together. Below is a description of the function we'll need to create to make this happen:

Step 1:  Create two lists, a copy of the question's shuffle order and an empty list to store the new order into

Step 2:  Transform the grouped list of labels provided to a grouped list of indexes and create a variable to store the position index of the first item in our grouped list relative to the shuffle order

Step 3:  Insert the list of grouped items in their specified order to this position 

Step 4:  Iterate through our copy of the shuffle order

Step 5:  As we go through this list, check to see if the item is either 1) the list we inserted into our copy (in step #2) or 2) a row item not found in the grouped list. If 1), concatenate the item to the new order list we are generating. If 2), append the item to the list we are generating.

Step 6:  The final step is to set the question's shuffle order to the new order list that we generated

Solution ( In Practice )

We will accomplish this task using <exec> blocks and Python code. Let's begin by creating a function called group_rows_with_order to perform the procedures written out in steps 1 - 6 above.

This function will accept two arguments: the question we are performing the manual shuffle on and a list of row labels relative to the rows that should be grouped together.

def group_rows_with_order( question, grouped_rows ):

When the time is right, we'll be able to call this function from our survey like this:

exec="group_rows_with_order( Q1, ['r5', 'r6', 'r7'] )"

Let's finish writing the function based on steps 1 - 6 listed above.

def group_rows_with_order( question, grouped_rows ):
    # Create two lists, a copy of the question's shuffle order
    # and an empty list to store the new order into
    current_order = [row.index for row in question.rows.order]
    new_order     = []

    # Transform the grouped list of labels provided to a grouped list of indexes
    # and create a variable to store the position index of the
    # first item in our grouped list relative to the shuffle order
    grouped_rows     = [question.attr(row).index for row in grouped_rows]
    first_item_index = current_order.index(grouped_rows[0])

    # Insert the list of grouped items in their specified order
    # to this position
    current_order.insert(first_item_index, grouped_rows)

    # Iterate through our copy of the shuffle order
    for row in current_order:
        # As we go through this list, check to see if the item is either
        # 1) the list we inserted into our copy or
        # 2) a row item not found in the grouped list
        if row == grouped_rows:
            # If 1), concatenate the item to the new order list we are generating
            new_order = new_order + row
        elif row not in grouped_rows:
            # If 2), append the item to the list we are generating
            new_order.append(row)

    # The final step is to set the question's shuffle order to the
    # new order list that we generated
    question.rows.order = new_order

That completes our function and fulfills the requirements specified for this task. As shown below, we can apply this function to our survey by adding it (without the comments) to an <exec> block that is called once when the survey is loaded. We can then call this function at our question using the <exec> attribute.

<exec when="init">
def group_rows_with_order( question, grouped_rows ):
    current_order = [row.index for row in question.rows.order]
    new_order     = []

    grouped_rows     = [question.attr(row).index for row in grouped_rows]
    first_item_index = current_order.index(grouped_rows[0])

    current_order.insert(first_item_index, grouped_rows)

    for row in current_order:
        if row == grouped_rows:
            new_order = new_order + row
        elif row not in grouped_rows:
            new_order.append(row)

    question.rows.order = new_order
</exec>

<radio label="Q1" shuffle="rows" exec="group_rows_with_order(Q1, ['r5', 'r6', 'r7'])">
    <title>Standard radio question</title>
    <comment>Please choose one.</comment>
    <row label="r1">r1</row>
    <row label="r2">r2</row>
    <row label="r3">r3</row>
    <row label="r4">r4</row>
    <row label="r5">r5</row>
    <row label="r6">r6</row>
    <row label="r7">r7</row>
    <row label="r8">r8</row>
</radio>

Result

We can assert that our function is working correctly by verifying that each time the question is loaded, rows 5, 6 and 7 are shuffled together and in the order specified.

Refresh the demo page to verify the correct result has been achieved.

Real World Application

This function would apply well to the question below given the following programmer note:

Programmer Note: Randomize rows. Keep Google products grouped together and in this order -- Mail, Search, Drive.

<checkbox label="Q4" shuffle="rows" exec="group_rows_with_order(Q4, ['r4', 'r3', 'r5'])">
    <title>Please select the services you currently use.</title>
    <comment>Select all that apply.</comment>
    <row label="r1">Bing Search</row>
    <row label="r2">DuckDuckGo</row>
    <row label="r3">Google Search</row>
    <row label="r4">Google Mail (GMail)</row>
    <row label="r5">Google Drive</row>
    <row label="r6">Yahoo Search</row>
    <row label="r7">Dropbox</row>
</radio>

This function can be stacked if we needed to group more than one set of rows. For example, if we also wanted to group "Bing Search" with "Yahoo Search" in the example above, we would just need to modify the question's exec attribute to call the function again for this additional set.

<checkbox label="Q4" shuffle="rows" exec="group_rows_with_order(Q4, ['r4', 'r3', 'r5']); group_rows_with_order(Q4, ['r1', 'r6']);">

4:  Shuffling Using shuffleBy

The attribute shuffleBy can be used to shuffle a set of rows in the same order as a previous question. To use shuffleBy, the number of rows in each question must be exactly the same. If the number of rows do not match, then you will need to take a couple extra steps to ensure the rows are shuffled in the same order.

There are two methods for shuffling in the same order provided below. You can use The where="none" Way if the number of rows is relatively small (less than 10 or so) or if the content of the rows do not match exactly. If the number of rows to shuffle by is large, then it will be easier to use The Pythonic Way.

4.1:  The where="none" Way

Context

You have a set of rows that need to be shuffled in the same order as a previous question's rows, but the number of rows do not match.

Example

In this example, there are two questions equipped with a different number of rows. The first question has 6 rows and the second has 3 rows. We need to shuffle the row elements in the second question in the same order as the first question.

In other words, items 1, 3 and 5 in the second question should be shown in the same order as 1, 3 and 5 in the first question.

<radio label="Q1" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">2</row>
    <row label="r3">3</row>
    <row label="r4">4</row>
    <row label="r5">5</row>
    <row label="r6">6</row>
</radio>

<radio label="Q2" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">3</row>
    <row label="r3">5</row>
</radio>

Without any modifications to the questions above, we will obtain the following result:

shuffle_shuffleby_base.png

Notice that rows 1, 3 and 5 in the second question are not in the same order as 1, 3 and 5 in the first question (e.g. 5 - 3 - 1 compared to 1 - 5 - 3). What we need to have happen is illustrated in the screenshot below:

shuffle_shuffleby_example1.png

The screenshot above shows the desired result. The second question's row elements are now shuffled in the same order as the first question's row elements (e.g. 5 - 3 - 1).

Solution (Pseudocode)

To achieve this result, we will be using the shuffleBy attribute. Since shuffleBy uses index values to line up the rows, we cannot use shuffleBy if the row counts do not match. However, we can get around this by matching the number of rows to the second question and hiding the rows that do not apply. Below is a description of the approach we'll take to make this happen:

Step 1:  Copy the rows from the first question to the second question in the same order

Step 2:  Hide all rows that should remain invisible to respondents by adding where="none" to each of the row tags

Step 3:  Add shuffleBy="Q1" to the second question where "Q1" is a reference to the original question

Solution (In Practice)

Attached below is the same code for the original sets of questions with the mismatched row counts. No changes have been applied yet.

<radio label="Q1" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">2</row>
    <row label="r3">3</row>
    <row label="r4">4</row>
    <row label="r5">5</row>
    <row label="r6">6</row>
</radio>

<radio label="Q2" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">3</row>
    <row label="r3">5</row>
</radio>

By going through steps 1 - 3 mentioned above, we end up with the following code with matching row counts. Notice that the second question is now equipped with the same number of rows as the first question.

<radio label="Q1" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">2</row>
    <row label="r3">3</row>
    <row label="r4">4</row>
    <row label="r5">5</row>
    <row label="r6">6</row>
</radio>

<radio label="Q2" shuffle="rows" shuffleBy="Q1">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2" where="none">2</row>
    <row label="r3">3</row>
    <row label="r4" where="none">4</row>
    <row label="r5">5</row>
    <row label="r6" where="none">6</row>
</radio>

where="none" was added to items 2, 4 and 6 because we only want to collect data on items 1, 3 and 5. The sole purpose of adding and hiding these row items is to match the number of rows for each question so that we can apply the shuffleBy="Q1" attribute.

Result

We can assert that we're getting the correct results if the row items in the second question always match the order they were presented in the first question.

Real World Application

This approach would apply well given the following questions and this programmer note:

Programmer Note: Shuffle rows. Show Google products in the same order for Q4 and Q5.

<checkbox label="Q4" shuffle="rows">
    <title>Please select the services you currently use.</title>
    <comment>Select all that apply.</comment>
    <row label="r1">Bing Search</row>
    <row label="r2">DuckDuckGo</row>
    <row label="r3">Google Search</row>
    <row label="r4">Google Mail (GMail)</row>
    <row label="r5">Google Drive</row>
    <row label="r6">Yahoo Search</row>
    <row label="r7">Dropbox</row>
</checkbox>

<radio label="Q5" shuffle="rows">
    <title>Which of these services is your favorite?</title>
    <comment>Please select one.</comment>
    <row label="r1">Google Search</row>
    <row label="r2">Google Mail (GMail)</row>
    <row label="r3">Google Drive</row>
</radio>

Demonstrated below, we can use the where="none" method discussed above and modify Q5 to fulfill the programmer note.

<radio label="Q5" shuffle="rows" shuffleBy="Q4">
    <title>Which of these services is your favorite?</title>
    <comment>Please select one.</comment>
    <row label="r1" where="none">Bing Search</row>
    <row label="r2" where="none">DuckDuckGo</row>
    <row label="r3">Google Search</row>
    <row label="r4">Google Mail (GMail)</row>
    <row label="r5">Google Drive</row>
    <row label="r6" where="none">Yahoo Search</row>
    <row label="r7" where="none">Dropbox</row>
</radio>

Conclusion & Thoughts

  • When it comes to shuffling a different number of rows based on a previous question, this method works. It would be more optimal, however, if we didn't need to add the extra rows. This could become very tedious with larger sets. Please continue to The Pythonic Way section for a method that achieves the same result and does not require where="none" to be added nor the same number of rows to be present.
  • In the examples above, writing shuffleBy="Q1" is exactly the same as writing exec="Q2.rows.order = Q1.rows.order".

4.2:  The Pythonic Way

Context

You have a set of rows that need to be shuffled in the same order as previous question's rows, but the number of rows do not match.

Example

In this example, there are two questions equipped with a different number of rows. The first question has 6 rows and the second has 3 rows. We need to shuffle the row elements in the second question in the same order as the first question.

In other words, items 1, 3 and 5 in the second question should be shown in the same order as 1, 3 and 5 in the first question.

<radio label="Q1" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">2</row>
    <row label="r3">3</row>
    <row label="r4">4</row>
    <row label="r5">5</row>
    <row label="r6">6</row>
</radio>

<radio label="Q2" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">3</row>
    <row label="r3">5</row>
</radio>

Without any modifications to the questions above, we get the following result:

shuffle_shuffleby_base.png

Notice that rows 1, 3 and 5 in the second question are not in the same order as 1, 3 and 5 in the first question (e.g. 5 - 3 - 1 compared to 1 - 5 - 3). What we need to have happen is illustrated in the screenshot below:

shuffle_shuffleby_example1.png

The second question's row elements are now shuffled in the same order as the first question's row elements (e.g. 5 - 3 - 1).

Solution (Pseudocode)

To achieve this result, we will use an <exec> block and Python code to manually set the shuffle order of the second question. Here's a description of the code we'll create:

Step 1:  Create a list containing the row text for each row in the current question

Step 2:  Create an empty list to generate the new shuffle order for the current question

Step 3:  Iterate through the source question's row order

Step 4:  When we come across a row that is present in the current question, add the row's index position (relative to the current question) to the generated list

Step 5:  Manually set the current question's row order to the new order generated

Solution (In Practice)

We'll create a function called shuffle_by that takes in two values: the question we're manually shuffling and the source question to shuffle by.

def shuffle_by( current_question, source_question ):

After we've created this function, we'll call it in our survey like this:

exec="shuffle_by( Q2, Q1 )"

Let's finish writing this function based on steps 1 - 5 listed above.

def shuffle_by( current_question, source_question ):
    # Create a list containing the row text for each row in the current question 
    current_order = [row.text for row in current_question.rows]

    # Create an empty list to generate the new shuffle order for the current question
    new_order = []

    # Iterate through the source question's row order
    for row in source_question.rows.order:
        # When we come across a row that is present in the current question,
        # add the row's index (relative to the current question) to the generated list
        if row.text in current_order:
            new_order.append( current_order.index( row.text ) )

    # Manually set the current question's row order to the new order generated
    current_question.rows.order = new_order

That completes our function and fulfills the requirements specified for this task. As shown below, we can apply this function to our survey by adding it (without the comments) to an <exec> block that is called once when the survey is loaded. We can then call this function at our question using the <exec> attribute.

<exec when="init">
def shuffle_by( current_question, source_question ):
    current_order = [row.text for row in current_question.rows]

    new_order = []

    for row in source_question.rows.order:
        if row.text in current_order:
            new_order.append( current_order.index( row.text ) )

    current_question.rows.order = new_order
</exec>

<radio label="Q1" shuffle="rows">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">2</row>
    <row label="r3">3</row>
    <row label="r4">4</row>
    <row label="r5">5</row>
    <row label="r6">6</row>
</radio>

<radio label="Q2" shuffle="rows" exec="shuffle_by(Q2, Q1)">
    <title>Please choose one.</title>
    <row label="r1">1</row>
    <row label="r2">3</row>
    <row label="r3">5</row>
</radio>

Result

We can assert that our function is working correctly if the second question's row items shuffle in the same order as presented in the first question.

Real World Application

This approach would apply well given the following questions and this programmer note:

Programmer Note: Shuffle rows. Show Google products in the same order for Q4 and Q5.

<checkbox label="Q4" shuffle="rows">
    <title>Please select the services you currently use.</title>
    <comment>Select all that apply.</comment>
    <row label="r1">Bing Search</row>
    <row label="r2">DuckDuckGo</row>
    <row label="r3">Google Search</row>
    <row label="r4">Google Mail (GMail)</row>
    <row label="r5">Google Drive</row>
    <row label="r6">Yahoo Search</row>
    <row label="r7">Dropbox</row>
</checkbox>

<radio label="Q5" shuffle="rows">
    <title>Which of these services is your favorite?</title>
    <comment>Please select one.</comment>
    <row label="r1">Google Search</row>
    <row label="r2">Google Mail (GMail)</row>
    <row label="r3">Google Drive</row>
</radio>

Demonstrated below, we can use the function we just created to fulfill the programmer note.

<radio label="Q5" shuffle="rows" exec="shuffle_by(Q5, Q4)">
    <title>Which of these services is your favorite?</title>
    <comment>Please select one.</comment>
    <row label="r1">Google Search</row>
    <row label="r2">Google Mail (GMail)</row>
    <row label="r3">Google Drive</row>
</radio>

This function would not correctly order the rows if the second the rows we are trying to shuffle do not have matching text values. For example, if Q5 in the example above had "Mail" instead of "Google Mail", but Q4 still had "Google Mail", the function would not work properly.

Conclusion & Thoughts

  • The shuffle_by function can be rewritten to reduce the line count, but as you can see below, it also reduces the readability of the code.
def shuffle_by( current_question, source_question ):
    current_order = [row.text for row in current_question.rows]
    current_question.rows.order = [current_order.index(row.text) for row in source_question.rows.order if row.text in current_order]
  • Este artigo foi útil?