oTree Forum >

before_next_page on WaitPage

#1 by simoncolumbus

TL;DR: Do WaitPages ignore before_next_page? If so, how can I assign values to a participant variable?

I have a multi-app project. In App 1, participants cast a vote. In App 2, I want to group them by arrival time, compute the group-level vote share, and assign the vote share to a participant variable for use across future rounds. I also want to assign a random treatment variable to each group and set it as a participant variable. The core of the second app looks like this (full MWE below/attached):

class Subsession(BaseSubsession):
    pass
def creating_session(subsession: Subsession):    
    treatments = itertools.cycle(['X', 'Y'])
    for group in subsession.get_groups():
        group.treatment = next(treatments)
class Group(BaseGroup):
    treatment = models.StringField()
    voteshare = models.IntegerField()
class Player(BasePlayer):
    pass
def get_votes(group: Group):
    # Compute outcome of vote
    players = group.get_players()
    votes = [p.participant.vote for p in players]
    group.voteshare = sum(votes)
class WaitVote(WaitPage):
#    group_by_arrival_time = True
    after_all_players_arrive = get_votes
    @staticmethod
    def before_next_page(player: Player, timeout_happened):
        group = player.group
        participant = player.participant
        participant.treatment = group.treatment
        participant.voteshare = group.voteshare
        
With this setup, group.treatment and group.voteshare are recorded in the data, but participant.treatment and participant.voteshare are not. If I move the code in before_next_page to the next, regular page, both participant variables are recorded correctly. Is there a way to do this directly on the WaitPage?

Second, I would also like to use group_by_arrival_time (here commented out). However, when I do this, neither of the two group variables are set. From other threads, I thought it was possible to use after_all_players_arrive below group_by_arrival time. This has left me puzzled about the correct order of creating_session, group_by_arrival_time, after_all_players_arrive, and before_next page. 

# settings.py

from os import environ
SESSION_CONFIG_DEFAULTS = dict(real_world_currency_per_point=1, 
                               participation_fee=0)
SESSION_CONFIGS = [dict(name='MWE', 
                        num_demo_participants=None, 
                        app_sequence=['vote_stage', 'main_stage'])] 
LANGUAGE_CODE = 'en'
REAL_WORLD_CURRENCY_CODE = 'GBP'
USE_POINTS = True
DEMO_PAGE_INTRO_HTML = ''
PARTICIPANT_FIELDS = ['treatment','vote','voteshare']
SESSION_FIELDS = []
ROOMS = []

ADMIN_USERNAME = 'admin'
# for security, best to set admin password in an environment variable
ADMIN_PASSWORD = environ.get('OTREE_ADMIN_PASSWORD')

SECRET_KEY = 'blahblah'

# if an app is included in SESSION_CONFIGS, you don't need to list it here
INSTALLED_APPS = ['otree']

# vote_stage/_init_.py

from otree.api import *
import random
c = cu

# Define constants
class C(BaseConstants):
    NAME_IN_URL = 'vote_stage'
    PLAYERS_PER_GROUP = None
    NUM_ROUNDS = 1
    
# Define parameters
class Subsession(BaseSubsession):
    pass
class Group(BaseGroup):
    pass
class Player(BasePlayer):
    vote = models.BooleanField(choices=[[True, 'A'], [False, 'B']], label='What do you choose?')
 
# Pages
class Vote(Page):
    form_model = 'player'
    form_fields = ['vote']    
    
    @staticmethod
    def before_next_page(player: Player, timeout_happened):
        participant = player.participant
        participant.vote = player.vote
    
# Page sequence
page_sequence = [Vote]

main_stage/_init_.py

from otree.api import *
import random
import itertools
c = cu

class C(BaseConstants):
    NAME_IN_URL = 'main_stage'
    PLAYERS_PER_GROUP = 4
    NUM_ROUNDS = 1
class Subsession(BaseSubsession):
    pass
def creating_session(subsession: Subsession):    
    treatments = itertools.cycle(['X', 'Y'])
    for group in subsession.get_groups():
        group.treatment = next(treatments)
class Group(BaseGroup):
    treatment = models.StringField()
    voteshare = models.IntegerField()
class Player(BasePlayer):
    pass
def get_votes(group: Group):
    # Compute outcome of vote
    players = group.get_players()
    votes = [p.participant.vote for p in players]
    group.voteshare = sum(votes)
class WaitVote(WaitPage):
#    group_by_arrival_time = True
    after_all_players_arrive = get_votes
    @staticmethod
    def before_next_page(player: Player, timeout_happened):
        group = player.group
        participant = player.participant
        participant.treatment = group.treatment
        participant.voteshare = group.voteshare
class VoteResult(Page):
    form_model = 'player'
    
page_sequence = [WaitVote, VoteResult]

#2 by Chris_oTree

Do it in after_all_players_arrive

#3 by simoncolumbus

Thanks, Chris. I have now (mostly) solved this by moving everything to after_all_players_arrive (see below, for future reference). 

However, I don't think it is possible to assign balanced treatments this way if also using group_by_arrival_time, is it? The itertools approach (https://otree.readthedocs.io/en/latest/treatments.html#balanced-treatment-groups) does not (seem to) work.

---

# main_stage/__init__.py

from otree.api import *
import random
import itertools
c = cu

class C(BaseConstants):
    NAME_IN_URL = 'main_stage'
    PLAYERS_PER_GROUP = 4
    NUM_ROUNDS = 1
class Subsession(BaseSubsession):
    pass
class Group(BaseGroup):
    treatment = models.StringField()
    voteshare = models.IntegerField()
class Player(BasePlayer):
    pass
class WaitVote(WaitPage):
    group_by_arrival_time = True
    @staticmethod
    def after_all_players_arrive(group: Group):
        # Set group treatment
        group.treatment = random.choice(['X', 'Y'])
        # Compute outcome of vote
        players = group.get_players()
        votes = [p.participant.vote for p in players]
        group.voteshare = sum(votes)
        # Set participant variables
        for p in players:
            participant = p.participant
            participant.treatment = group.treatment
            participant.voteshare = group.voteshare
class VoteResult(Page):
    form_model = 'player'
    
page_sequence = [WaitVote, VoteResult]

#4 by simoncolumbus

> However, I don't think it is possible to assign balanced treatments this way if also using group_by_arrival_time, is it? The itertools approach (https://otree.readthedocs.io/en/latest/treatments.html#balanced-treatment-groups) does not (seem to) work.

I've now used this workaround, just in case somebody might look for this:

class C(BaseConstants):
    TREATMENTS = ('X', 'Y')

class Subsession(BaseSubsession):
    counter = models.IntegerField(initial=0)
    
class WaitVote(WaitPage):
    group_by_arrival_time = True
    def after_all_players_arrive(group: Group):
        group.treatment = C.POLICIES[group.subsession.counter % 2]
        group.subsession.counter += 1

#5 by zwongo

Hi Simon, 
thanks for the question and for the workaround you posted - it's been really helpful. 
could you quickly clarify what is C.POLICIES in the code for your WaitVote page? 
thanks!
ZW

#6 by simoncolumbus (edited )

Hi ZW,

Looks like I mixed something up in my code there. C.POLICIES should be called C.TREATMENTS. It refers to the BaseConstant define above. 

C.TREATMENTS is a vector of possible treatments. My approach indexes this vector and assigns treatment X if the group.session.counter is even and treatment Y if it is odd. If you have more than two treatments, you'd need a corresponding way to index the vector of treatments.

--Simon

#7 by zwongo

Hi Simon,
Thanks for the lightning quick reply. 
I had figured out as much - but for some reason this particular workaround (with my 3 treatments and changing the mod function to mod3) does not seem to work for me. On the local demo, regardless of who I pass through first, they are all still lumped in one group with no treatment info stored in the group.treatment field.
In which class is your field for the group-level treatment information? Mine is still in the group class, but I am not sure that's the right place for it... 

And correct me if I am wrong, but the after_all_players_arrive is a session level method? So if I have a session with 12 players, would I neeed all 12 players to have arrived from app 1 in order for this to run? Or can I still form groups of 4 with the first four that arrive? 

Thanks for all your help!
ZW

#8 by zwongo

And here's a snippet of my init.py file: 

class C(BaseConstants):
    NAME_IN_URL = 'slider_feedback'
    PLAYERS_PER_GROUP = 4    
    NUM_ROUNDS = 1          
    treatments = ['No', 'Positive', 'Negative']

class Subsession(BaseSubsession):
    num_groups_created = models.IntegerField(initial=0)

class Group(BaseGroup):
    treatment = models.StringField()
    [...]
    
class Player(BasePlayer):
    #Task-related fields
    [...]

#PAGES

class Count_Start(Page):
    group_by_arrival_time = True 

    @staticmethod
    def after_all_players_arrive(group: Group):
        subsession = group.subsession
        index = subsession.num_groups_created % len(C.treatments)
        group.treatment = C.treatments[index]
        subsession.num_groups_created += 1

#9 by simoncolumbus

Hi ZW,

> In which class is your field for the group-level treatment information? Mine is still in the group class, but I am not sure that's the right place for it... 

Further down in my WaitPage, I set the treatment as a participant variable. This allows the treatment to be retained across rounds. There's two steps to this. First, add 'treatment' as a participant field in settings.py. Second, set participant.treatment to group.treatment:

players = group.get_players()
for p in players:
    participant = p.participant
    participant.treatment = group.treatment

> And correct me if I am wrong, but the after_all_players_arrive is a session level method? So if I have a session with 12 players, would I neeed all 12 players to have arrived from app 1 in order for this to run? Or can I still form groups of 4 with the first four that arrive? 

With after_all_players_arrive, that would be the case. Note that in my workaround, I use group_by_arrival_time. This will form groups of size PLAYERS_PER_GROUP (i.e., 4 in your case) once a sufficient number of players have arrived at the WaitPage.

--Simon

#10 by Daniel_Frey

I'm not sure but I think Count_Start should be a WaitPage so that after_all_players_arrive gets executed.

#11 by zwongo

Thanks @Simon - that has been really helpful. 
And @Daniel, indeed it only works if the page is a WaitPage! (part of why it wouldn't work!) 
Thank you all for contributing - this community is brilliant!

Write a reply

Set forum username