Skip to content

Grouping participants

Many experiments require participants to interact in pairs or small groups. uproot makes grouping easy with the GroupCreatingWait page type, which automatically forms groups as participants arrive.

Basic group formation

Create a wait page that groups participants by subclassing GroupCreatingWait and setting group_size:

class WaitForPartner(GroupCreatingWait):
    group_size = 2

When participants reach this page, they wait until enough others arrive to form a group. Once a group forms, all members advance to the next page together.

page_order = [
    WaitForPartner,
    GamePage,
    Results,
]

See the prisoners_dilemma example

Assigning roles with after_grouping

Use the after_grouping callback to assign roles or initialize group members when the group forms:

class GroupPlease(GroupCreatingWait):
    group_size = 2

    @classmethod
    def after_grouping(page, group):
        for player, is_dictator in zip(players(group), [True, False]):
            player.dictator = is_dictator

The callback receives the group object and runs exactly once when the group is created. All players in the group can then access their assigned attributes.

See the dictator_game example · trust_game example · ultimatum_game example · gift_exchange_game example

Using class attributes for role values

You can define role values as class attributes for cleaner code:

class GroupPlease(GroupCreatingWait):
    group_size = 2
    watch_values = (True, False)

    @classmethod
    def after_grouping(page, group):
        for player, watched in zip(players(group), page.watch_values):
            player.watched = watched

See the observed_diary example

Accessing group members

uproot provides several functions to access players within a group:

players(group)

Get all players in a group as a StorageBunch:

for player in players(group):
    player.payoff = 10

The StorageBunch supports iteration, filtering, and bulk operations:

# Sum contributions from all group members
total = sum(p.contribution for p in players(group))

# Filter players by attribute
cooperators = players(group).filter(_.cooperate == True)

# Find a specific player
dictator = players(group).find_one(dictator=True)

other_in_group(player)

For two-person groups, get the other player directly:

other = other_in_group(player)

if player.cooperate and other.cooperate:
    player.payoff = 10

This function raises an error if the group doesn't have exactly two members.

See the prisoners_dilemma example · twobytwo example

others_in_group(player)

For groups of any size, get all other members (excluding the current player):

others = others_in_group(player)

for other in others:
    notify(player, other, "Hello!")

Group-level data storage

Access the shared group storage with player.group:

class Calculate(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        # Store a value at the group level
        group.total_contribution = sum(p.contribution for p in players(group))

In templates, access group data:

<p>Your group contributed {{ player.group.total_contribution }} in total.</p>

Larger groups

For experiments with more than two players per group, simply increase group_size:

class GroupPlease(GroupCreatingWait):
    group_size = 3

Use the same patterns for accessing members:

class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        total = sum(p.contribution for p in players(group))

        for player in players(group):
            player.payoff = ENDOWMENT - player.contribution + MPCR * total

See the public_goods_game example · beauty_contest example · minimum_effort_game example

Manual group creation

For custom matching logic, you can create groups programmatically using create_group() and create_groups() instead of GroupCreatingWait. This is useful when you need to:

  • Match participants based on survey responses
  • Form groups of different sizes
  • Sort or balance groups by some criteria

Basic manual grouping

Use SynchronizingWait to wait for all participants, then create groups in the all_here callback:

class WaitForEveryone(SynchronizingWait):
    synchronize = "session"

    @classmethod
    def all_here(page, session):
        # Get all players and sort alphabetically by name
        all_players = sorted(players(session), key=lambda p: p.name)

        if len(all_players) == 1:
            # Only one player
            create_group(session, all_players)
        else:
            # Split into two groups of roughly equal size
            mid = len(all_players) // 2
            group_a = all_players[:mid]
            group_b = all_players[mid:]

            create_groups(session, [group_a, group_b])


class ShowGroup(Page):
    @classmethod
    def context(page, player):
        group_members = sorted(players(player.group), key=lambda p: p.name)
        return dict(
            group_name=player.group.name,
            group_members=group_members,
        )


page_order = [
    WaitForEveryone,
    ShowGroup,
]

See the grouping_test example

create_group()

Creates a single group from a list of players:

# Create a group from specific players
gid = create_group(session, [player1, player2])

# With a custom group name
gid = create_group(session, members, gname="custom_name")

# Allow reassigning players already in groups
gid = create_group(session, members, overwrite=True)

create_groups()

Creates multiple groups at once:

# Create pairs from a list of players
all_players = list(players(session))
pairs = [[all_players[i], all_players[i+1]] for i in range(0, len(all_players), 2)]
gids = create_groups(session, pairs)

Grouping by attribute

Match participants based on their responses:

class WaitAndMatch(SynchronizingWait):
    synchronize = "session"

    @classmethod
    def all_here(page, session):
        # Separate players by their preference
        prefer_a = [p for p in players(session) if p.preference == "A"]
        prefer_b = [p for p in players(session) if p.preference == "B"]

        # Match players with different preferences
        for p1, p2 in zip(prefer_a, prefer_b):
            create_group(session, [p1, p2])

Complete example: prisoner's dilemma

Here's a complete two-player game showing group formation, decision collection, and payoff calculation:

class GroupPlease(GroupCreatingWait):
    group_size = 2


class Dilemma(Page):
    fields = dict(
        cooperate=RadioField(
            label="Do you wish to cooperate?",
            choices=[(True, "Yes"), (False, "No")],
        ),
    )


def set_payoff(player):
    other = other_in_group(player)

    match player.cooperate, other.cooperate:
        case True, True:
            player.payoff = 10
        case True, False:
            player.payoff = 0
        case False, True:
            player.payoff = 15
        case False, False:
            player.payoff = 3


class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        for player in players(group):
            set_payoff(player)


class Results(Page):
    @classmethod
    def context(page, player):
        return dict(other=other_in_group(player))


page_order = [
    GroupPlease,
    Dilemma,
    Sync,
    Results,
]

See the full prisoners_dilemma example

Summary

Function Purpose
GroupCreatingWait Wait page that forms groups automatically
group_size Number of players per group
after_grouping(page, group) Callback when group forms
players(group) Get all players in a group
other_in_group(player) Get the other player (2-person groups)
others_in_group(player) Get all other players in the group
player.group Access group-level storage
create_group() Programmatically create a group