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