#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!