#1 by amarchal
Hello, I am working on a two stage market experiment with 88 rounds. It really struggles with the number of rounds so I was looking for insight on ways to make it more efficient. Thanks! (_init_ code below) from otree.api import * import array as arr import random as random doc = """ This is the code for a two stage market experiment. Each market has two producers who may coordinate using a chat that opens at the beginning of each block the market exists. In the first stage participants choose whether they want to invest in a technology. Based on the first stage decision participants make a second stage price decision. Private payoffs are determined by the investment and price decisions made by both producers in the market. """ # These functions are used to determine Constant parameters. Can be replaced with hard code if needed. # Function to get the final (aggregate) round in each SG def cumsum(lst): total = 0 new = [] for ele in lst: total += ele new.append(total) return new # Function to get return a list of the first (cumulative) round in each SG def cumstart(lst): total = 1 new = [1] for ele in lst: total += ele new.append(total) return new # Function to get the number of rounds played in each SG using the roll termination and block size as inputs def sgterm(rollter, block): ter = [] for ele in rollter: while ele % block > 0: ele += 1 ter.append(ele) return ter class SuperGames(ExtraModel): Rounds_Per_SG = models.IntegerField() class C(BaseConstants): NAME_IN_URL = 'main' PLAYERS_PER_GROUP = 2 # Group Size BLOCK_SIZE = 4 # SG block size - FLEXIBLE! NUM_SG = 10 # Number of matches played - FLEXIBLE! CONTINUATION_PROBABILITY = 87.5 # Experiment continuation probability - FLEXIBLE! INVESTMENT_FAIL_PROBABILITY = 20 # Probability the investment decision is successful - FLEXIBLE! EXCHANGE_RATE = 100 # eFrancs to USD - FLEXIBLE! ROLL_TERMINATION = [4, 19, 5, 1, 13, 1, 5, 4, 5, 10] # Actual payoff termination (seeded!) # ROLL_TERMINATION = [4, 5] # Actual payoff termination - USE FOR TESTING ONLY! ROLLS_LIST = [14.4, 42.2, 12.5, 97.9, 22.1, 54.6, 24.9, 60.5, 72.5, 51.9, 69.5, 54.9, 7.6, 9.3, 43.6, 35.1, 26.5, 69.2, 56.5, 38.9, 85.8, 1.6, 88.6, 29.0, 67.9, 46.9, 5.3, 40.1, 88.1, 21.3, 36.0, 36.8, 91.1, 53.0, 96.4, 29.2, 36.5, 11.9, 28.1, 30.0, 87.0, 43.0, 86.2, 3.5, 56.5, 42.6, 29.3, 22.1, 90.5, 54.5, 17.8, 69.0, 92.6, 31.1, 8.1, 13.3, 76.3, 80.8, 65.6, 71.8, 91.2, 89.4, 25.1, 81.9, 56.3, 48.3, 74.2, 99.8, 25.0, 16.4, 14.1, 8.8, 99.3, 10.4, 23.4, 59.8, 19.2, 11.5, 52.5, 29.5, 55.7, 35.0, 45.3, 61.7, 70.5, 89.3, 81.7, 57.4] # Realized rolls # ROLLS_LIST = [14.4, 42.2, 12.5, 97.9, 22.1, 54.6, 24.9, 60.5, 72.5, 51.9, 69.5, 54.9, 7.6, 9.3, 43.6, 35.1, 26.5, # 69.2, 56.5, 38.9, 85.8, 1.6, 88.6, 29.0] # Realized Rolls - USE FOR TESTING ONLY! ROUNDS_PER_SG = sgterm(ROLL_TERMINATION, BLOCK_SIZE) # Number of rounds played - function found above SG_ENDS = cumsum(ROUNDS_PER_SG) # List of (cumulative) final rounds for each SG - function found above SG_STARTS = cumstart(ROUNDS_PER_SG) # List of (cumulative) first rounds for each SG - function found above NUM_ROUNDS = sum(ROUNDS_PER_SG) # How many (cumulative) rounds CHAT_LENGTH = 120 DD_PAYOFFS_1B = [[92, 133, 175, 217, 258, 300, 342, 383], [150, 200, 250, 300, 350, 400, 450, 500], [175, 233, 292, 350, 408, 467, 525, 583], [167, 233, 300, 367, 433, 500, 567, 633], [125, 200, 275, 350, 425, 500, 575, 650], [50, 133, 217, 300, 383, 467, 550, 633], [-58, 33, 125, 217, 308, 400, 492, 583], [-200, -100, 0, 100, 200, 300, 400, 500]] DI_PAYOFFS_1B = [[50, 92, 133, 175, 217, 258, 300, 342], [100, 150, 200, 250, 300, 350, 400, 450], [117, 175, 233, 292, 350, 408, 467, 525], [100, 167, 233, 300, 367, 433, 500, 567], [50, 125, 200, 275, 350, 425, 500, 575], [-33, 50, 133, 217, 300, 383, 467, 550], [-150, -58, 33, 125, 217, 308, 400, 492], [-300, -200, -100, 0, 100, 200, 300, 400]] ID_PAYOFFS_1B = [[275, 350, 425, 500, 575, 650, 725, 800], [283, 367, 450, 533, 617, 700, 783, 867], [258, 350, 442, 533, 625, 717, 808, 900], [200, 300, 400, 500, 600, 700, 800, 900], [108, 217, 325, 433, 542, 650, 758, 867], [-17, 100, 217, 333, 450, 567, 683, 800], [-175, -50, 75, 200, 325, 450, 575, 700], [-367, -233, -100, 33, 167, 300, 433, 567]] II_PAYOFFS_1B = [[200, 275, 350, 425, 500, 575, 650, 725], [200, 283, 367, 450, 533, 617, 700, 783], [167, 258, 350, 442, 533, 625, 717, 808], [100, 200, 300, 400, 500, 600, 700, 800], [0, 108, 217, 325, 433, 542, 650, 758], [-133, -17, 100, 217, 333, 450, 567, 683], [-300, -175, -50, 75, 200, 325, 450, 575], [-500, -367, -233, -100, 33, 167, 300, 433]] DD_PAYOFFS_1C = [[21, 65, 108, 151, 195, 238, 281, 325], [91, 143, 195, 246, 298, 350, 401, 453], [128, 188, 248, 308, 368, 428, 488, 548], [131, 200, 268, 336, 405, 473, 541, 610], [101, 178, 255, 331, 408, 485, 561, 638], [38, 123, 208, 293, 378, 463, 548, 633], [-59, 35, 128, 221, 315, 408, 501, 595], [-189, -87, 15, 116, 218, 320, 421, 523]] DI_PAYOFFS_1C = [[-22, 21, 65, 108, 151, 195, 238, 281], [40, 91, 143, 195, 246, 298, 350, 401], [68, 128, 188, 248, 308, 368, 428, 488], [63, 131, 200, 268, 336, 405, 473, 541], [25, 101, 178, 255, 331, 408, 485, 561], [-47, 38, 123, 208, 293, 378, 463, 548], [-152, -59, 35, 128, 221, 315, 408, 501], [-290, -189, -87, 15, 116, 218, 320, 421]] ID_PAYOFFS_1C = [[58, 126, 195, 263, 331, 400, 468, 536], [95, 171, 248, 325, 401, 478, 555, 631], [98, 183, 268, 353, 438, 523, 608, 693], [68, 161, 255, 348, 441, 535, 628, 721], [5, 106, 208, 310, 411, 513, 615, 716], [-92, 18, 128, 238, 348, 458, 568, 678], [-222, -104, 15, 133, 251, 370, 488, 606], [-385, -259, -132, -5, 121, 248, 375, 501]] II_PAYOFFS_1C = [[-10, 58, 126, 195, 263, 331, 400, 468], [18, 95, 171, 248, 325, 401, 478, 555], [13, 98, 183, 268, 353, 438, 523, 608], [-25, 68, 161, 255, 348, 441, 535, 628], [-97, 5, 106, 208, 310, 411, 513, 615], [-202, -92, 18, 128, 238, 348, 458, 568], [-340, -222, -104, 15, 133, 251, 370, 488], [-512, -385, -259, -132, -5, 121, 248, 375]] class Subsession(BaseSubsession): sg = models.IntegerField() # SG number period = models.IntegerField() # Round in SG is_first_period = models.BooleanField() # First SG periods boolean is_last_period = models.BooleanField() # Last SG periods boolean class Group(BaseGroup): chat_page = models.IntegerField() class Player(BasePlayer): first_stage_decision = models.IntegerField( choices=[[0, "Don't Invest"], [1, "Invest"]], label="Do you want to try and invest?", ) first_stage_string = models.StringField() partner_first_stage_string = models.StringField() second_stage_game = models.IntegerField(initial=0) second_stage_decision = models.IntegerField( choices=[1, 2, 3, 4, 5, 6, 7, 8], label="What price do you want to set for this round?", ) partner_second_stage_decision = models.IntegerField() ss_round_number = models.IntegerField(initial=0) investment_draw = models.IntegerField(initial=0) investment_successful = models.IntegerField(initial=0) round_draw = models.FloatField() round_payoff = models.IntegerField() match_payoff = models.IntegerField(initial=0) class Message(ExtraModel): group = models.Link(Group) sender = models.Link(Player) text = models.StringField() def to_dict(msg: Message): return dict(sender=msg.sender.id_in_group, text=msg.text) # FUNCTIONS # Function creating the session. Des the grouping and parsing for each SG def creating_session(subsession): # Randomly grouping participants each SG for ss in subsession.in_rounds(1, C.NUM_ROUNDS): # Looping over each round if ss.round_number in C.SG_STARTS: # Randomly grouping if the round number is in the SG starts list ss.group_randomly() else: # Matching groups to the previous round if not in the SG starts list ss.group_like_round(ss.round_number - 1) # Defining each SG with first and last game markers if subsession.round_number == 1: sg = 1 period = 1 for ss in subsession.in_rounds(1, C.NUM_ROUNDS): # Looping over each round ss.sg = sg # Defining the SG number ss.period = period # Defining the period number in the SG is_last_period = ss.round_number in C.SG_ENDS is_first_period = ss.round_number in C.SG_STARTS ss.is_last_period = is_last_period # Defining whether period is final in SG ss.is_first_period = is_first_period # Defining whether period is first in SG if is_last_period: # Resetting in-SG indexing if final period of SG sg += 1 period = 1 else: # Moving in-SG indexing forward period += 1 # Function determining which second stage game is going to be played for each person def set_game(group: Group): # Getting each player players = group.get_players() # If players invest, determining if the investment is successful and getting the appropriate corresponding game second_stage_game = 0 for p in players: # Determine if the investment was successful (moved here for IV estimation) round_check = p.round_number + (C.BLOCK_SIZE - 1) if round_check % C.BLOCK_SIZE == 0: p.investment_draw = random.randint(1, 100) else: previous = p.in_round(p.round_number - 1) p.investment_draw = previous.investment_draw # If they invested if p.first_stage_decision == 1 and p.investment_draw > C.INVESTMENT_FAIL_PROBABILITY: # If this is a round they make a successful investment p.investment_successful = 1 second_stage_game += 1 # If only one person successfully invested, assign the correct code for which game each plays for p in players: if second_stage_game == 1 and p.investment_draw > C.INVESTMENT_FAIL_PROBABILITY and p.first_stage_decision == 1: p.second_stage_game = 2 elif second_stage_game == 1: p.second_stage_game = 1 # Assigning the correct code if both successfully invest elif second_stage_game == 2: p.second_stage_game = 3 # Decoding the first stage decision for html prompts for p in players: if p.first_stage_decision == 1 and p.investment_draw > C.INVESTMENT_FAIL_PROBABILITY: p.first_stage_string = "Successful Investment" else: p.first_stage_string = "No Successful Investment" # Function calculating the round and aggregate payoffs for each player by group def set_payoffs(group: Group): players = group.get_players() # Recalling the matrix for the game that was played then locating the realized payoff session = group.session if session.config['payoffs_1b'] is True: dd_payoffs = C.DD_PAYOFFS_1B di_payoffs = C.DI_PAYOFFS_1B id_payoffs = C.ID_PAYOFFS_1B ii_payoffs = C.II_PAYOFFS_1B else: dd_payoffs = C.DD_PAYOFFS_1C di_payoffs = C.DI_PAYOFFS_1C id_payoffs = C.ID_PAYOFFS_1C ii_payoffs = C.II_PAYOFFS_1C for p in players: partner = p.get_others_in_group()[0] p.partner_first_stage_string = partner.first_stage_string p.partner_second_stage_decision = partner.second_stage_decision if p.second_stage_game == 0: p.round_payoff = dd_payoffs[p.second_stage_decision - 1][partner.second_stage_decision - 1] elif p.second_stage_game == 1: p.round_payoff = di_payoffs[p.second_stage_decision - 1][partner.second_stage_decision - 1] elif p.second_stage_game == 2: p.round_payoff = id_payoffs[p.second_stage_decision - 1][partner.second_stage_decision - 1] else: p.round_payoff = ii_payoffs[p.second_stage_decision - 1][partner.second_stage_decision - 1] # PAGES class InstructionsWaitPage(WaitPage): # Temporarily assigning the second stage game to match the previous round for # all rounds that aren't the first in a block. @staticmethod def after_all_players_arrive(group: Group): if group.round_number not in C.SG_STARTS: players = group.get_players() for p in players: previous_roll = p.in_round(p.round_number - 1) if p.round_number + C.BLOCK_SIZE - 1 % C.BLOCK_SIZE > 0: p.second_stage_game = previous_roll.second_stage_game class ChatPage(Page): timeout_seconds = C.CHAT_LENGTH # How long chat lasts # Only unlock this page if it's the first round of a block and in the chat_page treatment @staticmethod def is_displayed(player: Player): session = player.session round_check = player.round_number + (C.BLOCK_SIZE - 1) return round_check % C.BLOCK_SIZE == 0 and session.config['chat_page'] is True # Accessing JS for the chat live_page @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def live_method(player: Player, data): my_id = player.id_in_group group = player.group # Saving every text in the chat if 'text' in data: text = data['text'] msg = Message.create(group=group, sender=player, text=text) return {0: [to_dict(msg)]} return {my_id: [to_dict(msg) for msg in Message.filter(group=group)]} class ChatWaitPage(WaitPage): # Pull through first_stage_decision for players in rounds that don't start a new block @staticmethod def after_all_players_arrive(group: Group): players = group.get_players() for p in players: p.round_draw = C.ROLLS_LIST[p.round_number - 1] * 10 start_round = p.round_number starts = arr.array('i', C.SG_STARTS) while start_round not in starts: start_round -= 1 current_round = p.round_number - start_round + 1 p.ss_round_number = current_round # Only assigning if the modulus is non-zero (not a new block) round_check = p.round_number + (C.BLOCK_SIZE - 1) if round_check % C.BLOCK_SIZE > 0: previous_decision = p.in_round(p.round_number - 1) p.first_stage_decision = previous_decision.first_stage_decision class FirstStageDecision(Page): # First stage form_field form_model = 'player' form_fields = ['first_stage_decision'] # Only displayed at the start of new blocks @staticmethod def is_displayed(player: Player): round_check = player.round_number + (C.BLOCK_SIZE - 1) return round_check % C.BLOCK_SIZE == 0 @staticmethod def vars_for_template(player: Player): start_round = player.round_number starts = arr.array('i', C.SG_STARTS) while start_round not in starts: start_round -= 1 game_number = starts.index(start_round) + 1 if player.round_number in C.SG_STARTS: return dict(sg_start=1, sg_number=game_number) else: return dict(sg_start=0, continuation_probability=C.CONTINUATION_PROBABILITY, block_size=C.BLOCK_SIZE, draw_rounds=player.in_rounds(start_round, player.round_number-1), draws=C.ROLLS_LIST[player.round_number-1]) class SecondStageDecisionWait(WaitPage): # Determine which matrix will be used by recalling the 'set_game' function above after_all_players_arrive = 'set_game' class SecondStageDecision(Page): form_model = 'player' form_fields = ['second_stage_decision'] @staticmethod def vars_for_template(player: Player): session = player.session if session.config['payoffs_1b'] is True: return dict(treatment=1) else: return dict(treatment=0) # There are four pages, one for each stage one decision combo. Only the relevant one is used each round. class SecondStageWaitPage(WaitPage): # Set the round payoffs using the 'set_payoffs' function above after_all_players_arrive = 'set_payoffs' class RoundResults(Page): # Getting info for the round results tables @staticmethod def vars_for_template(player: Player): # Finding which round the SG started by taking the current round number and counting backwards until it gets # to a round number in the SG Starts list. Needed for the history of previous rounds. start_round = player.round_number starts = arr.array('i', C.SG_STARTS) while start_round not in starts: start_round -= 1 current_round = player.round_number - start_round + 1 num_rounds = len(player.in_rounds(start_round, player.round_number)) # Getting the counterparts info and determining which rounds to display in the history chart return dict(start_round=start_round, curr_round=current_round, num_rounds=num_rounds, partner=player.get_others_in_group()[0], all_rounds=reversed(player.in_rounds(start_round, player.round_number))) # Calculating SG payoffs and adding it to the player payoff if it's the final round of the match @staticmethod def before_next_page(player: Player, timeout_happened): if player.round_number in C.SG_ENDS: # Only used if the round is a final round # Referencing which SG is finishing ends = arr.array('i', C.SG_ENDS) sg = ends.index(player.round_number) # SG number actual_start = C.SG_STARTS[sg] # Actual round start actual_end = C.SG_STARTS[sg] + C.ROLL_TERMINATION[sg] - 1 # Actual payoff end (not end of block) # Calculating match payoff for only paid rounds match_payoff = sum([p.round_payoff for p in player.in_rounds(actual_start, actual_end)]) player.match_payoff = match_payoff player.payoff += cu(match_payoff) class ResultsWait(WaitPage): # Hold players up to find match results @staticmethod def is_displayed(player: Player): return player.round_number in C.SG_ENDS class MatchResults(Page): # Match results page @staticmethod def is_displayed(player: Player): return player.round_number in C.SG_ENDS # Recalling the round the payoffs terminated in to inform the participants @staticmethod def vars_for_template(player: Player): ends = arr.array('i', C.SG_ENDS) sg = ends.index(player.round_number) end = C.ROLL_TERMINATION[sg] current_sg = sg + 1 next_sg = current_sg + 1 start_round = player.round_number starts = arr.array('i', C.SG_STARTS) while start_round not in starts: start_round -= 1 final_roll = C.ROLLS_LIST[start_round+end-2] return dict(final_round=end, current_sg=current_sg, next_sg=next_sg, num_sg=C.NUM_SG, draw_rounds=player.in_rounds(start_round, player.round_number), continuation_probability=C.CONTINUATION_PROBABILITY, final_roll=final_roll) # End of part 1 page class EndResults(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS # Calculating participants' aggregate payoff @staticmethod def vars_for_template(player: Player): part_payoff = sum([p.match_payoff for p in player.in_all_rounds()])/C.EXCHANGE_RATE return dict(part_payoff=part_payoff) page_sequence = [InstructionsWaitPage, ChatPage, ChatWaitPage, FirstStageDecision, SecondStageDecisionWait, SecondStageDecision, SecondStageWaitPage, RoundResults, ResultsWait, MatchResults, EndResults]