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.
Groups are lazy¶
Groups are only created when participants reach a GroupCreatingWait page. uproot does not pre-assign groups when a session starts—grouping happens at runtime as participants arrive. This means the number of groups depends on how many participants actually show up, not on how many were expected.
Basic group formation¶
Create a wait page that groups participants by subclassing GroupCreatingWait and setting group_size:
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.
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(group.players, [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(group.players, page.watch_values):
player.watched = watched
See the observed_diary example
Accessing group members¶
group.players¶
Get all players in a group as a StorageBunch using the virtual field:
The StorageBunch supports iteration, filtering, and bulk operations.
Basic iteration¶
# Sum contributions from all group members
total = sum(p.contribution for p in group.players)
# Unpack directly (e.g. in SynchronizingWait.all_here)
p1, p2 = group.players
Filtering with _¶
The _ symbol is a field referent — a placeholder that stands for "each player" in a filter expression. When you write _.cooperate == True, uproot builds a comparison object that gets evaluated against each player in the collection.
# Players who cooperated
cooperators = group.players.filter(_.cooperate == True)
# Players with a score above 50
high_scorers = session.players.filter(_.score > 50)
# Combine multiple conditions (all must be true)
eligible = session.players.filter(_.present == True, _.age >= 18)
_ supports all comparison operators: ==, !=, >, >=, <, <=. You can also chain attribute access — _.group.round refers to each player's group's round field.
Note
_ builds a lazy comparison, so _.active alone (without a comparison operator) tests for truthiness. To check for False, write _.active == False explicitly.
Finding a single player¶
find_one() returns exactly one player matching the criteria. It raises an error if zero or multiple players match.
Extracting values with each¶
each() collects a field from every player into a list:
# Get all contributions as a list
contributions = group.players.each("contribution")
# → [10, 20, 15]
# Multiple fields return named tuples
data = group.players.each("name", "score", simplify=False)
# → [data(name='Alice', score=10), data(name='Bob', score=20)]
Bulk assignment with assign¶
assign() sets a field on every player from an iterable:
Running a function on all players with apply¶
apply() calls a function once per player:
See Synchronizing progress for a full example.
player.other_in_group¶
For two-person groups, access the other player via the virtual field:
Raises an error if the group doesn't have exactly two members.
See the prisoners_dilemma example · twobytwo example
player.others_in_group¶
For groups of any size, get all other members (excluding the current player):
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 group.players)
In templates, access group data:
Larger groups¶
For experiments with more than two players per group, simply increase group_size:
Use the same patterns for accessing members:
class Sync(SynchronizingWait):
@classmethod
def all_here(page, group):
total = sum(p.contribution for p in group.players)
for player in group.players:
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(session.players, 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 templatevars(page, player):
group_members = sorted(player.group.players, 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 · grouping_test_arbitrary_size example · grouping_test_one_spare example · grouping_via_GroupCreatingWait_and_move_to_page 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(session.players)
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 session.players if p.preference == "A"]
prefer_b = [p for p in session.players 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 = player.other_in_group
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 group.players:
set_payoff(player)
class Results(Page):
pass
page_order = [
GroupPlease,
Dilemma,
Sync,
Results,
]
See the full prisoners_dilemma example
Summary¶
| Purpose | |
|---|---|
GroupCreatingWait |
Wait page that forms groups automatically |
group_size |
Number of players per group |
after_grouping(page, group) |
Callback when group forms |
group.players |
All players in a group |
player.other_in_group |
The other player (2-person groups) |
player.others_in_group |
All other players in the group |
session.players |
All players in a session |
player.group |
Access group-level storage |
create_group() |
Programmatically create a group |