oTree Forum >

Improving App Performance.

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

Write a reply

Set forum username