experiment-template/Tutorial oTree.md
duartegoncalvesds 73fc2121bf first commit
2026-03-12 21:05:38 +00:00

11 KiB
Raw Blame History

Lecture: Introduction to oTree

Goal: Understand the architecture of an oTree application and build a basic Bayesian game.


0. Preliminaries:


1. oTree

What is oTree?

oTree is an open-source, Starlette-based framework for implementing interactive experiments in the browser.

  • Logic: Written in Python.
  • Interface: Written in HTML/Starlette template tags.
  • Data Storage: Automatic integration with PostgreSQL or SQLite.
  • Deployment: Easy to host on Heroku or local servers.

Installation + project creation

Install

python -m pip install otree
otree --version

Create a project

otree startproject myproject
cd myproject
otree devserver

Then open: http://localhost:8000/

Create an app inside the project

otree startapp my_app

File layout (important ones)

Inside the project:

  • settings.py — session configs, installed apps, currency settings, etc.
  • _static/ — CSS/JS (optional)
  • _templates/ — global templates (optional)
  • my_app/ — your app code

Inside an app:

  • __init__.py — main app logic (models + pages + constants)
  • templates/my_app/*.html — participant-facing pages
  • tests.py — automated tests/bots (optional but recommended)

The Core Structure

An oTree project is organised into Apps. Each app contains:

  • __init__.py: The "brain." Defines your models, variables, and logic.
  • Pages.py: Controls the sequence of what the participant sees.
  • Templates (.html): The visual interface for each page.

The Hierarchy

  1. Session: The entire group of participants.
  2. Subsession: A specific round within an app.
  3. Group: A collection of players interacting (e.g., in a prisoner's dilemma; we won't do this today).
  4. Player: The individual unit.
  5. Pages: The screens participants see (forms, instructions, results).

Data live in the database. Each Player has fields that get saved automatically.


Defining Variables

Variables are defined in __init__.py under three classes: Constants, Subsession, Group, and Player.

class Constants(BaseConstants):
    NAME_IN_URL = 'Decision'
    PLAYERS_PER_GROUP = None
    NUM_PRACTICE_ROUNDS = 2
    NUM_PAYMENT_ROUNDS = 3
    NUM_ROUNDS = NUM_PRACTICE_ROUNDS + NUM_PAYMENT_ROUNDS

class Player(BasePlayer):
    decision = models.IntegerField()
    belief_1 = models.FloatField(min=0, max=100)
    captcha_attempt_1 = models.StringField(label="", initial="")
    

More types of fields here https://otree.readthedocs.io/en/latest/models.html#fields.

Forms and validation

Basic:

class Decision(Page):
    form_model = "player"
    form_fields = ["choice"]

Cross-field validation:

class Player(BasePlayer):
    a = models.IntegerField()
    b = models.IntegerField()

class SomePage(Page):
    form_model = "player"
    form_fields = ["a", "b"]

    @staticmethod
    def error_message(player, values):
        if values["a"] + values["b"] != 10:
            return "a + b must equal 10."

Where to put logic: common hooks

  • Subsession.creating_session()
    Initialise per-session/per-round variables, assign treatments, form groups.

  • Page.is_displayed(player)
    Conditional page display (by round/treatment/role).

  • Page.vars_for_template(player)
    Pass computed values to the template.

  • Page.error_message(player, values)
    Validate across multiple fields.

  • Page.before_next_page(player, timeout_happened)
    Compute payoffs, set state, write vars.

  • Group.set_payoffs() (your own method)
    Common pattern: compute payoffs after everyone submits.


Game Logic & Functions

Logic is triggered by events.

Determining if page is displayed

class TaskFeedback(Page):
    @staticmethod
    def is_displayed(player):
        return (
            player.round_number <= C.NUM_PRACTICE_ROUNDS and
            not player.participant.vars.get('failed_captcha', False)
        )

Setting values in particular rounds

def creating_session(subsession: Subsession):
    for player in subsession.get_players():
        if player.round_number == 1:
            treatments_index = list(range(len(C.payoffs_list)))
            random.shuffle(treatments_index)
            for r in range(C.NUM_PAYMENT_ROUNDS):
                player.in_round(C.NUM_PRACTICE_ROUNDS+1+r).treatment_index = treatments_index[r]

Useful built-in methods: https://otree.readthedocs.io/en/latest/models.html#built-in-fields-and-methods.


The User Interface

Templates use HTML combined with oTree tags to display data dynamically.

Contribute.html

{{ block title }}
    Round {{ payment_round_number }} out of {{ C.NUM_PAYMENT_ROUNDS }}
{{ endblock }}

Live pages (real-time interaction)

For truly interactive real-time tasks (e.g., chat, continuous-time trading), oTree supports live pages (WebSocket-style). Basic pattern:

class MyLivePage(Page):
    live_method = "live_handler"

    @staticmethod
    def live_handler(player, data):
        # data is a dict sent from JS
        return {player.id_in_group: dict(message="received")}

You also write JS in the template to send/receive messages.

See more here: https://otree.readthedocs.io/en/latest/multiplayer/intro.html.


Running the Server

To see your work in the browser:

  1. Initialise: otree devserver 8000
  2. Access: Open http://localhost:8000
  3. Data: Go to the Admin tab to monitor real-time results and export CSV/Excel files.

Quick Reference Table

Command Purpose
otree startproject <name> Create a new project folder
otree startapp <name> Create a new app within the project
otree devserver Run the local testing server
otree zip Package the app for server upload

Important: Always test your app using to ensure your payoff logic works before bringing in real participants.


2. Deployment

For testing/class projects, focus first on:

  • local devserver (otree devserver);
  • exporting data reliably.

Common option:

  1. oTree app on GitHub.
  2. Host on heroku.com.
  3. Recruit on UCL ELFE or Prolific.com.

Template already fitted to deal with this pipeline. The setting (in config; available when creating a session) completionlink is where you should put the prolific link. Prolific ID should be saved in participant labels.


On Heroku

  • Create new app
  • Deploy from GitHub; manual deploy; then turn on automatic deployments
  • Add web dyno (GitHub education gives free dynos); worker dyno not needed
  • Add-on: postgresql (cheapest for testing)

Running for real:

  • Performance M dynos with auto-scalling with max latency of 500ms
  • Postgresql Standard 2
  • Config vars: OTREE_AUTH_LEVEL: STUDY; OTREE_PRODUCTION: 1; OTREE_ADMIN_PASSWORD: whateveryouwant

Downgrading postgresql is a real pain. Here's my cheatsheet:

Procedure to copy and remove previous database on heroku
# Step 0. Login
heroku login

# Step 1. Provision a new database
heroku addons:create heroku-postgresql:essential-0 --app example-app

# Step 2. Enter maintenance mode to prevent database writes
heroku maintenance:on --app appname

# Step 3. Transfer data to the new database
## Check name and colour; or on website; it takes a while to provision, give it 2-3 minutes for new database to to show up
heroku addons --all
heroku config --app example-app
## Copy
heroku pg:copy app_name_to_copy_from::HEROKU_POSTGRESQL_COLOUR_TO_COPY_FROM HEROKU_POSTGRESQL_COLOUR_TO_COPY_TO --app app_name_to_copy_to

# Step 4. Promote the new database
heroku pg:promote HEROKU_POSTGRESQL_COLOUR_TO_PROMOTE -a appname

# Step 5. Destroy previous database
heroku addons:destroy HEROKU_POSTGRESQL_COLOUR --confirm appname

# Step 6. Exit maintenance mode
heroku maintenance:off --app appname


Example

heroku login
heroku maintenance:on --app southern-path-2
heroku addons:create heroku-postgresql:essential-0 --app southern-path-2
heroku addons --all
# This gets you the colours of the database; note that it takes a bit to provision, so they won't show immediately
heroku config --app southern-path-2
heroku pg:copy southern-path-2::DATABASE HEROKU_POSTGRESQL_PINK_URL --app southern-path-2 --confirm southern-path-2
heroku pg:promote postgresql-clear-38905 -a southern-path-2
heroku addons:destroy postgresql-round-22954 --confirm southern-path-2
heroku maintenance:off --app southern-path-2


On Prolific

My typical study name: Survey on Decision-Making (up to £X.00/Nmin with bonus)

Internal name: Acronym YYYY.MM.DD

My typical description:

This survey seeks to understand how individuals make choices and is conducted by academic researchers. 

You can earn up to £X.00 for Nmin.

You will earn a minimum of £Y.00. 

Please keep in mind that how much you will receive will depend on your own choices and the choices of other participants. The payment scheme is designed to encourage you to think carefully about your choices.
  • Study label: None
  • Device requirements: Desktop only
  • Labels: None
  • Limit participant access: Depends on your budget
  • What's the URL of your study? https://HEROKUAPPNAME.herokuapp.com/room/prolific?participant_label={{%PROLIFIC_PID%}}&STUDY_ID={{%STUDY_ID%}}&SESSION_ID={{%SESSION_ID%}}
  • How do you want to record IDs? URL parameters
  • Completion paths: Manually review
  • Screeners:
    • Current Country of Residence: United Kingdom (better sample)
    • Approval Rate: 95100
    • Fluent languages: English
    • Exclude participants from other studies: any pilot and other waves of the same study that you've ran
  • Do participants require a login & password to access your survey tool? No
  • Total times a participant can complete your study: once
  • Reject exceptionally fast submissions: NO!

On ELFE

  • Login to main terminal.
  • Run the app in production mode, create ELFE session.
  • Turn on all terminals and open the link.
  • Test the Qualtrics questionnaire at the end.
  • Test (elfe.lab.run)[elfe.lab.run].
  • Schedule session on (elfe.lab.run)[elfe.lab.run] and invite participants.

On the day

  • Login to main terminal.
  • Run the app in production mode, create ELFE session.
  • Turn on all terminals and open the link.
  • Sit participants and allocate them on the app.
  • Let them start after all seated.
  • Take note of anything unusual.
  • Show timer and remind participants of the time every half hour or so.
  • Make sure everyone filled out the survey at the end before leaving (if they finished).
  • Pay participants (lab manager, via Wise account).