from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range, ) import random author = 'Claudia Cerrone, Yoan Hermstrüwer & Giulia Baldini' doc = """ Matching with Consent: An Experimental Study """ # Parameters for experimental sessions # 120 participants in each treatment, i.e. a total of 480 participants # 1 pilot with 30 participants # 8 sessions with 60 participants each # 20 rounds in each session # 10 Euros per participant on average class Constants(BaseConstants): name_in_url = 'matching_consent' num_rounds = 2 # TODO: Change to the desired number of rounds additional_table = True # TODO: Change if you do/don't want the preference table to appear in survey results = True # TODO: Decide if you want to show the final results instructions = True # TODO: Decide if you want to show practice questions matching_group = 10 # TODO: Change size of matching groups timer_minutes_instructions = 25 # TODO: Number of minutes in the instructions timer_minutes_survey = 5 # TODO: Number of minutes in the survey timer_minutes_result = 3 # TODO: Number of minutes in the result soft_timer_seconds_instructions = 1200 # TODO: Seconds before the soft timer in the instructions soft_timer_seconds_survey = 240 # TODO: Seconds before the soft timer in survey points_vector = [c(25), c(18), c(12), c(7), c(3)] # TODO: Change according to the points values # Change according to given preference list positional_array = ['First', 'Second', 'Third', 'Fourth', 'Fifth'] expected_control_answer = [5, 0, 5, 1, 0, 0, 0, 1, 0, 0] # TODO: Change according to the correct answers preferences = { # TODO: Change to have different preferences for the students 1: ['A', 'C', 'D', 'B', 'E'], 2: ['B', 'D', 'A', 'E', 'C'], 3: ['D', 'A', 'B', 'C', 'E'], 4: ['C', 'A', 'B', 'E', 'D'], 5: ['C', 'B', 'A', 'D', 'E'] } players_per_group = len(preferences) priorities = { # TODO: Change to have different priorities for the students 'A': [2, 4, 1, 5, 3], 'B': [4, 1, 2, 3, 5], 'C': [3, 2, 4, 5, 1], 'D': [4, 5, 3, 2, 1], 'E': [1, 3, 2, 5, 4] } # Worked out example example_points = [c(10), c(6), c(3), c(1)] example_preferences = { 1: ['A', 'D', 'B', 'C'], 2: ['A', 'B', 'C', 'D'], 3: ['A', 'B', 'C', 'D'], 4: ['C', 'A', 'B', 'D'] } example_priorities = { 'A': [4, 1, 2, 3], 'B': [2, 3, 1, 4], 'C': [3, 4, 2, 1], 'D': [1, 4, 3, 2] } example_positional_array = positional_array[:-1] # Change according to example # Changed preference list when Student 1 waives her priority example_preferences_waived = example_preferences.copy() example_preferences_waived[1] = ['', 'D', 'B', 'C'] choices_array = [] for p in priorities: curr = [p, ('School ' + p)] choices_array.append(curr) Control_template = 'consent/Control.html' Decision_template = 'consent/Decision.html' Example_template = 'consent/Example.html' Intro_template = 'consent/Intro.html' Procedure_template = 'consent/Procedure.html' def chunks(total, chunk_size): for i in range(0, len(total), chunk_size): yield total[i:i + chunk_size] class Subsession(BaseSubsession): def group_randomly_by_matching_groups(self): new_matrix = [] total_matching_groups = int(self.session.num_participants / Constants.matching_group) for i in range(0, total_matching_groups): range_low = (i * Constants.matching_group) + 1 first_group = [] second_group = [] for j in range(0, Constants.players_per_group): if random.randint(0, 1): first_group.append(range_low + j) second_group.append(range_low + j + Constants.players_per_group) else: second_group.append(range_low + j) first_group.append(range_low + j + Constants.players_per_group) new_matrix.append(first_group) new_matrix.append(second_group) print(new_matrix) self.set_group_matrix(new_matrix) def creating_session(self): if Constants.matching_group is None: self.group_randomly(fixed_id_in_group=True) else: self.group_randomly_by_matching_groups() if self.round_number == 1: for p in self.get_players(): p.participant.vars['true_preferences'] = Constants.preferences[p.id_in_group] def index_of(array, element): index = 0 for a in array: if a == element: return index index += 1 class Group(BaseGroup): def deferred_acceptance(self, given_preferences, consent_list=None): students_app = {} schools_app = {} given_preferences_copy = {} flag = True interrupters_list = [] # Set everything to -1: At the beginning, nobody is matched # Save the preferences given by the players for student in Constants.preferences: students_app[student] = -1 given_preferences_copy[student] = given_preferences[student].copy() for school in Constants.priorities: schools_app[school] = -1 while flag: flag = False for student_id in students_app: # For every student if students_app[student_id] == -1 and len(given_preferences_copy[student_id]) > 0: # If the student is not matched and still has preferences flag = True # Take the first element out of his preferences, and delete it from the list liked_school = given_preferences_copy[student_id].pop(0) if schools_app[liked_school] == -1: # If this school is not matched students_app[student_id] = liked_school # Match the student with the school schools_app[liked_school] = student_id # Match the school with this student elif index_of(Constants.priorities[liked_school], schools_app[liked_school]) > \ index_of(Constants.priorities[liked_school], student_id): # If the current student matched by the school has a larger index in the priority list than me if consent_list and consent_list[schools_app[liked_school]]: interrupters_list.append((schools_app[liked_school], liked_school)) students_app[schools_app[liked_school]] = -1 # Set the school to unmatched students_app[student_id] = liked_school # Match the student with the school schools_app[liked_school] = student_id # Match the school with this student # print(schools_app) # Print the result at every round, needed for debug return students_app, interrupters_list def deferred_acceptance_consent(self, given_preferences, consent_list): while True: ranking, interrupters = self.deferred_acceptance(given_preferences, consent_list) # print(interrupters) if len(interrupters) == 0: break interrupting_student = interrupters[-1][0] # Find the id of the student interrupter interrupting_schools = [pair[1] for pair in interrupters if pair[0] == interrupting_student] for school in interrupting_schools: given_preferences[interrupting_student].remove(school) return ranking def set_payoffs(self): given_preferences = {} # Save the preferences given by the players for student in Constants.preferences: given_preferences[student] = self.get_player_by_id(student).participant.vars['preferences'].copy() # Create consent list consent_list = {} for p in self.get_players(): if p.participant.vars['given_consent']: consent_list[p.id_in_group] = 1 else: consent_list[p.id_in_group] = 0 # Choose method according to treatment assignment if self.session.config['current_treatment'] == 1: students_ranking, none = self.deferred_acceptance(given_preferences) else: students_ranking = self.deferred_acceptance_consent(given_preferences, consent_list) schools_allocation = [students_ranking[i] for i in range(1, len(Constants.preferences) + 1)] # print(students_ranking) points_allocation = [] for student_id in range(1, len(Constants.preferences) + 1): p = self.get_player_by_id(student_id) p.participant.vars['school_allocation'] = schools_allocation school = students_ranking[student_id] pos_school = p.participant.vars['true_preferences'].index(school) p.payoff = Constants.points_vector[pos_school] points_allocation.append(p.payoff) if self.round_number == 1: p.participant.vars['possible_payoffs'] = [p.payoff] else: p.participant.vars['possible_payoffs'].append(p.payoff) for p in self.get_players(): p.participant.vars['points_allocation'] = points_allocation def compute_rows(points_vector, preferences): points_preferences = {} for i in range(0, len(points_vector)): points_preferences[points_vector[i]] = [] for student in preferences: points_preferences[points_vector[i]].append(preferences[student][i]) return points_preferences def return_all_vals(): string_num_1 = 'four' # These two strings fill the text in Procedure string_num_2 = 'five' preferences_row = compute_rows(Constants.points_vector, Constants.preferences) priorities_row = compute_rows(Constants.positional_array, Constants.priorities) students = list(Constants.preferences.keys()) schools = list(Constants.priorities.keys()) ex_preferences_row = compute_rows(Constants.example_points, Constants.example_preferences) ex_preferences_row_waived = compute_rows(Constants.example_points, Constants.example_preferences_waived) ex_priorities_row = compute_rows(Constants.example_positional_array, Constants.example_priorities) ex_students = list(Constants.example_preferences.keys()) ex_schools = list(Constants.example_priorities.keys()) return string_num_1, string_num_2, preferences_row, priorities_row, students, schools, ex_preferences_row, \ ex_priorities_row, ex_preferences_row_waived, ex_students, ex_schools class Player(BasePlayer): participant_vars_dump = models.LongStringField() session_vars_dump = models.LongStringField() ################## # STUDENT CHOICES first_choice = models.StringField(verbose_name="First choice:", choices=Constants.choices_array, initial=None) second_choice = models.StringField(verbose_name="Second choice:", choices=Constants.choices_array, initial=None) third_choice = models.StringField(verbose_name="Third choice:", choices=Constants.choices_array, initial=None) fourth_choice = models.StringField(verbose_name="Fourth choice:", choices=Constants.choices_array, initial=None) fifth_choice = models.StringField(verbose_name="Fifth choice:", choices=Constants.choices_array, initial=None) consent = models.IntegerField(default=0, blank=True) age = models.IntegerField(verbose_name="What is your age?", choices=range(16, 101), initial=None ) study = models.IntegerField(verbose_name="What is your field of study?", choices=[ [0, "Agricultural Science"], [1, "Art"], [2, "Biology"], [3, "Chemistry"], [4, "Computer Science"], [5, "Economics"], [6, "Engineering"], [7, "Geography/Geology"], [8, "History"], [9, "Law"], [10, "Mathematics"], [11, "Medicine/Pharmaceutical Studies"], [12, "Philosophy"], [13, "Philology (e.g. German)"], [14, "Physics"], [15, "Political Science"], [16, "Psychology"], [17, "Regional Studies (e.g. Asia Studies)"], [18, "Sociology"], [19, "Theology/Religion Studies"], [20, "Other"]], initial=None) gender = models.IntegerField(verbose_name="What is your gender?", choices=[ [0, "Female"], [1, "Male"], [2, "Diverse"]], initial=None) consent_reason = models.IntegerField( verbose_name="In one or more rounds, you did not consent to waiving your priorities. Why?", choices=[ [0, "I do not want others to do better, regardless of how I do."], [1, "I do not want others to do better, if I cannot do better too."], [2, "I thought that consenting would make me worse off."], [3, "I thought that other participants in my group would not consent either."], [4, "Other reasons."]], initial=None) reason_free_text = models.LongStringField(verbose_name="If you selected 'Other reasons', please specify them:", blank=True) object_reason = models.IntegerField( verbose_name="In one or more rounds, you objected to waiving your priorities. Why?", choices=[ [0, "I do not want others to do better, regardless of how I do."], [1, "I do not want others to do better, if I cannot do better too."], [2, "I thought that not objecting would make me worse off."], [3, "I thought that other participants in my group would object too."], [4, "Other reasons."]], initial=None) consent_other = models.IntegerField( verbose_name="Do you think that the most participants in your group consented in each round?", choices=[ [0, "No"], [1, "Yes"]], initial=None) object_other = models.IntegerField( verbose_name="Do you think that the most participants in your group objected in each round?", choices=[ [0, "No"], [1, "Yes"]], initial=None) truthful = models.IntegerField(verbose_name= "Do you think you could be better off by submitting a list of schools that did not correspond to the ranking provided in the table?", choices=[ [0, "No"], [1, "Yes"]], initial=None) comments = models.LongStringField(verbose_name="Further comments on the instructions and the experiment in general:", blank=True) ################## # CONTROL QUESTIONS question1 = models.IntegerField(verbose_name="1. How many participants are there in your group in each round?", choices=range(1, 11), initial=None) question2 = models.IntegerField(verbose_name="2. Do participants in your group remain the same in each round?", choices=[ [0, "No"], [1, "Yes"], [2, "The instructions do not tell"] ], initial=None) question3 = models.IntegerField(verbose_name="3. If you are admitted at School A, how many points do you earn?", choices=[ [0, 25], [1, 18], [2, 12], [3, 7], [4, 3], [5, "It depends on my student type"] ], initial=None) question4 = models.IntegerField(verbose_name="4. Do you keep your student type in each round?", choices=[ [0, "No"], [1, "Yes"], [2, "The instructions do not tell"] ], initial=None) question5 = models.IntegerField(verbose_name="5. Does each school have the same priorities over students?", choices=[ [0, "No"], [1, "Yes"], [2, "The instructions do not tell"] ], initial=None) question6 = models.IntegerField(verbose_name= "6. If you are admitted at a school, can another student be simultaneously be admitted at the same school?", choices=[ [0, "No"], [1, "Yes"], [2, "The instructions do not tell"] ], initial=None) question7 = models.IntegerField(verbose_name="7. Is the admission final at the end of each step?", choices=[ [0, "No"], [1, "Yes"], [2, "The instructions do not tell"] ], initial=None) question8 = models.IntegerField(verbose_name= "8. If a school does not reject you at any of the steps, does this mean that you are finally admitted at that school?", choices=[ [0, "No"], [1, "Yes"], [2, "The instructions do not tell"] ], initial=None) question9 = models.IntegerField( verbose_name="9. Is your final admission affected by whether you consent to waiving your priorities?", choices=[ [0, "No"], [1, "Yes"] ], initial=None) question10 = models.IntegerField( verbose_name="9. Is your final admission affected by whether you object to waiving your priorities?", choices=[ [0, "No"], [1, "Yes"] ], initial=None) ################## def draw_sample(self): self.participant.vars['drawn_sample'] = random.sample(range(0, Constants.num_rounds), 2) def check_droupped_out(self): group = self.get_others_in_group() for p in group: if 'timed_out' in p.participant.vars: return True return False