from otree.api import * import random class C(BaseConstants): NAME_IN_URL = 'econi' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 TREATMENTS = ['gender', 'ethnicity', 'baseline'] MIXED_TREATMENT_WEIGHTS = { 'baseline': 1, 'ethnicity': 2, 'gender': 2, } CHOICES = ['A', 'B'] GENDERS = ['Male', 'Female'] ETHNICITIES = ['East Asian', 'White'] BELIEF_PAYOFFS = {0: 80, 1: 64, 2: 32} MAX_ATTEMPTS = 3 QUIZ_MINUTES = 3 class Subsession(BaseSubsession): def creating_session(self): self.group_randomly() _init_identity_choice_stats(self.session) config_treatment_raw = self.session.config.get('treatment') config_treatment = ( str(config_treatment_raw).strip().lower() if config_treatment_raw is not None else None ) use_weighted_assignment = ( config_treatment in (None, '', 'mixed') or config_treatment not in C.TREATMENTS ) weighted_treatments = list(C.MIXED_TREATMENT_WEIGHTS.keys()) weighted_values = list(C.MIXED_TREATMENT_WEIGHTS.values()) for p in self.get_players(): if use_weighted_assignment: treatment = random.choices( weighted_treatments, weights=weighted_values, k=1, )[0] else: treatment = config_treatment p.treatment = treatment p.participant.vars['treatment'] = treatment if treatment in ('gender', 'ethnicity'): order = [True, False] random.shuffle(order) for idx, same_group in enumerate(order, start=1): DecisionRow.create( player=p, treatment=treatment, same_group=same_group, seq=idx, ) else: DecisionRow.create( player=p, treatment=treatment, same_group=None, seq=1, ) def vars_for_admin_report(self): stats = self.session.vars.get('identity_choice_stats', {}) def rel_rows(treatment, identities): out = [] tstats = stats.get(treatment, {}) for identity in identities: id_stats = tstats.get(identity, {}) for relation in ['same', 'diff']: bucket = id_stats.get(relation, {}) out.append( dict( identity=identity, relation=relation, A=bucket.get('A', 0), B=bucket.get('B', 0), total=bucket.get('total', 0), ) ) return out baseline_bucket = stats.get('baseline', {}).get('all', {}) gender_ids = ['Male', 'Female'] ethnicity_ids = ['White', 'East Asian'] return dict( identity_choice_stats=stats, gender_relation_rows=rel_rows('gender', gender_ids), ethnicity_relation_rows=rel_rows('ethnicity', ethnicity_ids), baseline_row=dict( A=baseline_bucket.get('A', 0), B=baseline_bucket.get('B', 0), total=baseline_bucket.get('total', 0), ), ) class Group(BaseGroup): pass class Player(BasePlayer): treatment = models.StringField(initial='') quiz_attempts = models.IntegerField(initial=0) quiz_wrong_total = models.IntegerField(initial=0) quiz_failed = models.BooleanField(initial=False) quiz_duration_sec = models.IntegerField(initial=0) # matched_partner_id = models.IntegerField(blank=True) # payoff_source = models.StringField(blank=True) # belief_selected_seq = models.IntegerField(blank=True) # belief_actual_x = models.IntegerField(blank=True) # ---- Payoff matrix quiz (stag hunt) ---- quiz_q1 = models.IntegerField( choices=[[1, 'True'], [2, 'False']], widget=widgets.RadioSelect, label='Your bonus payment depends on the decision you make and the decision made by another participant randomly matched with you.' ) quiz_q2 = models.IntegerField( choices=[[1, '0'], [2, '40'], [3, '60']], widget=widgets.RadioSelect, label="If both you and the other participant choose X, what is your bonus payment?" ) quiz_q3 = models.IntegerField( choices=[[1, '0'], [2, '40'], [3, '60']], widget=widgets.RadioSelect, label="If both you and the other participant choose Y, what is the other participant's bonus payment?" ) quiz_q4 = models.IntegerField( choices=[[1, '60'], [2, '40'], [3, '0']], widget=widgets.RadioSelect, label="If you choose X and the other participant chooses Y, what is your bonus payment?" ) quiz_q5 = models.IntegerField( choices=[[1, '60'], [2, '40'], [3, '0']], widget=widgets.RadioSelect, label="If you choose Y and the other participant chooses X, what is the other participant's bonus payment?" ) # # ---- Belief quiz ---- # belief_q1 = models.IntegerField( # choices=[[1, '100'], [2, '80'], [3, '40'], [4, '0']], # widget=widgets.RadioSelect, # label='Suppose the true number who chose X is 7, and you answered 2. How much do you earn?' # ) # belief_q3 = models.IntegerField( # choices=[[1, '100'], [2, '80'], [3, '40'], [4, '0']], # widget=widgets.RadioSelect, # label='Suppose the true number who chose X is 3 out of 10, and you answered 5. How much do you earn?' # ) # belief_q2 = models.IntegerField( # choices=[[1, 'One of your answers'], [2, 'Both answers'], [3, 'None']], # widget=widgets.RadioSelect, # label='How many of your answers are used to determine your bonus payment?' # ) # ---- Pre-experiment survey (treatment-specific) ---- gender = models.StringField( choices=[ 'Male', 'Female', 'Non-binary', ], widget=widgets.RadioSelect, label='What is your gender?' ) ethnicity = models.StringField( choices=[ 'White', 'East Asian', 'Southeast Asian', 'South Asian', 'Black or African American', 'Hispanic/Latino', 'Native American or Alaska Native', 'Middle Eastern or North African', 'Pacific Islander', 'Other', ], widget=widgets.RadioSelect, label='What is your ethnicity?' ) # Ethnic priming ethnic_generations = models.StringField( choices=['First Generation', 'Second Generation', 'More than Two Generations'], widget=widgets.RadioSelect, label='How many generations has your family lived in this country?' ) ethnic_identify_strength = models.StringField( choices=['Not at all', 'Somewhat', 'Very strongly'], widget=widgets.RadioSelect, label='How strongly do you identify yourself with this country?' ) # ethnic_group = models.StringField( # choices=[ # 'White', # 'East Asian', # 'Southeast Asian', # 'South Asian', # 'Black or African American', # 'Hispanic/Latino', # 'Native American or Alaska Native', # 'Middle Eastern or North African', # 'Pacific Islander', # 'Other', # ], # widget=widgets.RadioSelect, # initial='', # label='What is your ethnicity?' # ) ethnic_group_other = models.StringField( blank=True, label='Please specify:' ) ethnic_roommate_pref = models.StringField( choices=[ 'Same race/ethnicity', 'Different race/ethnicity', 'I am equally happy with a roommate of my own or of a different race/ethnicity', ], widget=widgets.RadioSelect, label='If you could live with any roommate, would you prefer to live with a roommate of your own race/ethnicity or a different race/ethnicity?' ) ethnic_adv_same_1 = models.StringField( label='List one advantage of having a roommate of your own race/ethnicity.') ethnic_adv_same_2 = models.StringField(blank=True,label='List a second advantage of having a roommate of your own race/ethnicity.') ethnic_adv_diff_1 = models.StringField( label='List one advantage of having a roommate of a different race/ethnicity.') ethnic_adv_diff_2 = models.StringField(blank=True,label='List a second advantage of having a roommate of a different race/ethnicity.') # Gender priming # gender_self = models.StringField( # choices=['Male', 'Female'], # widget=widgets.RadioSelect, # # blank=True, # label='What is your gender?' # ) gender_identify_strength = models.StringField( choices=['Not at all', 'Somewhat', 'Very strongly'], widget=widgets.RadioSelect, # blank=True, label='How strongly do you identify with your gender?' ) gender_importance = models.StringField( choices=['Not at all', 'Somewhat', 'Very important'], widget=widgets.RadioSelect, # blank=True, label='How important is your gender in describing who you are?' ) gender_living_pref = models.StringField( choices=[ 'Mixed-sex living arrangement', 'Single-sex living arrangement', 'I am equally happy with a mixed-sex or a single-sex living arrangement', ], widget=widgets.RadioSelect, # blank=True, label='If you could live anywhere, would you prefer a mixed-sex or a single-sex living arrangement?' ) gender_adv_coed_1 = models.StringField(label='List one advantage of living in a mixed-sex setting.') gender_adv_coed_2 = models.StringField(blank=True, label='List a second advantage of living in a mixed-sex setting.') gender_adv_single_1 = models.StringField(label='List one advantage of living in a single-sex setting.') gender_adv_single_2 = models.StringField(blank=True, label='List a second advantage of living in a single-sex setting.') # Control treatment # control_age = models.IntegerField(blank=True, min=13,max=90, label='How old are you?') control_cable_tv = models.StringField( choices=['Yes', 'No'], widget=widgets.RadioSelect, # blank=True, label='Do you have cable television?' ) control_roommate = models.StringField( choices=['Yes', 'No'], widget=widgets.RadioSelect, # blank=True, label='Do you have a roommate?' ) control_roommate_pref = models.StringField( choices=[ 'A quiet roommate', 'A social roommate', 'I am equally happy with a quiet or a social roommate', ], widget=widgets.RadioSelect, # blank=True, label='If you had to pick, would you prefer a quiet roommate or one who is social?' ) control_adv_quiet_1 = models.StringField(label='List one advantage of living with a quiet roommate.') control_adv_quiet_2 = models.StringField(blank=True, label='List a second advantage of living with a quiet roommate.') control_adv_social_1 = models.StringField(label='List one advantage of living with a social roommate.') control_adv_social_2 = models.StringField(blank=True, label='List a second advantage of living with a social roommate.') def live_method(self, data): msg_type = data.get('type') if msg_type == 'init_choice': payload = _next_choice_payload(self) return {self.id_in_group: payload} if msg_type == 'submit_choice': seq = int(data.get('seq')) choice = data.get('choice') if choice not in C.CHOICES: return {self.id_in_group: dict(type='error', msg='invalid_choice')} rows = [r for r in DecisionRow.filter(player=self) if r.seq == seq] if not rows: return {self.id_in_group: dict(type='error', msg='invalid_question')} row = rows[0] # Guard against duplicate submit events from the frontend. if row.choice in C.CHOICES: payload = _next_choice_payload(self) return {self.id_in_group: payload} row.choice = choice # Update session-level choice statistics used later in payment. stats = self.session.vars.get('identity_choice_stats') if not stats: _init_identity_choice_stats(self.session) stats = self.session.vars['identity_choice_stats'] treatment = self.treatment if treatment == 'baseline': bucket = stats.get('baseline', {}).get('all') if bucket: bucket[choice] += 1 bucket['total'] += 1 self.session.vars['identity_choice_stats'] = stats elif treatment in ['gender', 'ethnicity']: if treatment == 'gender': identity = self.field_maybe_none('gender') else: identity = self.field_maybe_none('ethnicity') if identity: treatment_stats = stats.get(treatment, {}) if identity in treatment_stats: relation = 'same' if row.same_group else 'diff' rel_bucket = treatment_stats[identity][relation] rel_bucket[choice] += 1 rel_bucket['total'] += 1 # Re-assign to persist nested mutation in session vars. self.session.vars['identity_choice_stats'] = stats payload = _next_choice_payload(self) return {self.id_in_group: payload} if msg_type == 'init_belief': payload = _next_belief_payload(self) return {self.id_in_group: payload} if msg_type == 'submit_belief': seq = int(data.get('seq')) belief_raw = data.get('belief') try: belief = int(belief_raw) except Exception: belief = None if belief is None or belief < 0 or belief > 10: return {self.id_in_group: dict(type='error', msg='invalid_belief')} rows = [r for r in DecisionRow.filter(player=self) if r.seq == seq] if not rows: return {self.id_in_group: dict(type='error', msg='invalid_question')} rows[0].belief = belief payload = _next_belief_payload(self) return {self.id_in_group: payload} return {self.id_in_group: dict(type='noop')} class DecisionRow(ExtraModel): player = models.Link(Player) treatment = models.StringField() same_group = models.BooleanField(blank=True) seq = models.IntegerField() choice = models.StringField(blank=True) belief = models.IntegerField(min=0, max=10, blank=True) def _rows_for_player(player: Player): rows = [r for r in DecisionRow.filter(player=player)] rows.sort(key=lambda r: r.seq) return rows def _next_choice_payload(player: Player): rows = _rows_for_player(player) idx = next((i for i, r in enumerate(rows) if r.choice in (None, '')), None) if idx is None: return dict(type='done') q = rows[idx] return dict( type='show', mode='choice', question=dict( seq=q.seq, index=idx + 1, total=len(rows), same_group=q.same_group, treatment=q.treatment, ), ) def _next_belief_payload(player: Player): rows = _rows_for_player(player) idx = next((i for i, r in enumerate(rows) if r.belief is None), None) if idx is None: return dict(type='done') q = rows[idx] return dict( type='show', mode='belief', question=dict( seq=q.seq, index=idx + 1, total=len(rows), same_group=q.same_group, treatment=q.treatment, ), ) def choice_payoff(choice_self: str, choice_other: str) -> int: if choice_self == 'A' and choice_other == 'A': return 60 if choice_self == 'A' and choice_other == 'B': return 0 if choice_self == 'B' and choice_other == 'A': return 40 return 40 def _blank_counter(): return {'A': 0, 'B': 0, 'total': 0} def _init_identity_choice_stats(session): if 'identity_choice_stats' in session.vars: return session.vars['identity_choice_stats'] = { 'gender': { 'Male': { 'same': _blank_counter(), 'diff': _blank_counter(), }, 'Female': { 'same': _blank_counter(), 'diff': _blank_counter(), }, }, 'ethnicity': { 'White': { 'same': _blank_counter(), 'diff': _blank_counter(), }, 'East Asian': { 'same': _blank_counter(), 'diff': _blank_counter(), }, }, 'baseline': { 'all': _blank_counter(), }, } def _sync_identity_state_to_participant_vars(player: Player): rows = [r for r in DecisionRow.filter(player=player)] rows.sort(key=lambda r: r.seq) state_rows = [] for r in rows: state_rows.append( dict( seq=r.seq, same_group=r.same_group, choice=r.choice or '', belief=r.belief, ) ) player.participant.vars['identity_state'] = dict( treatment=player.treatment, identity=_identity_label_for_player(player), qualified=bool(player.participant.vars.get('qualified', True)) and not player.quiz_failed, rows=state_rows, choices_complete=bool(rows) and all((r.choice in C.CHOICES) for r in rows), ) def _identity_label_for_player(p: Player) -> str: if p.treatment == 'gender': return p.field_maybe_none('gender') or '' if p.treatment in ('ethnicity', 'race'): ethnic = p.field_maybe_none('ethnicity') return ethnic if ethnic in ['White', 'East Asian'] else '' return 'Baseline' def custom_export(players): # Export DecisionRow ExtraModel data in a flat row format. yield [ 'session_code', 'participant_code', 'treatment', 'identity', 'seq', 'same_group', 'choice', 'belief', ] for p in players: identity = _identity_label_for_player(p) rows = [r for r in DecisionRow.filter(player=p)] rows.sort(key=lambda r: r.seq) for r in rows: has_choice = r.choice not in (None, '') has_belief = r.belief is not None if not (has_choice or has_belief): continue yield [ p.session.code, p.participant.code, p.treatment, identity, r.seq, r.same_group, r.choice, r.belief, ]