From time to time I’m using oTree as a framework to implement computer-based lab or online experiments for researchers at the WZB. Most experiments include a survey and it’s always quite a hassle to efficiently implement a questionnaire with oTree as its API is mostly designed for more complex things such as multiplayer games and controlled behavioral experiments. For example, for a simple survey question you would need to implement three steps: 1) add a field to the Player
model; 2) set up a page and the form fields to display; and 3) set up a template for that page.
I’ve created a Python package named otreeutils (available for installation via pip on PyPI) that contains several utility functions to tackle some of oTree’s deficiencies and it was initially released in November 2016. Since then I added new features from time to time, for example the ability to integrate “custom data models” more easily into oTree, allowing live monitoring and exporting data from such models. I published a short paper that describes how using custom data models and otreeutils helps when trying to collect data of dynamically determined quantity.1
I recently added a new feature to this package that further facilitates creating surveys, especially when using Likert scale inputs. In this post I’m giving a short example on how to use otreeutils for this purpose.
Defining a questionnaire in models.py
The major advantage of using otreeutils for surveys is that you specify your whole questionnaire at a single point instead of scattering it across three different files as in standard oTree (models.py
, pages.py
and your HTML template). With otreeutils, you define the questionnaire only in models.py
by using a single data structure. This data structure is a sequence that holds the definition of questions and their input field per page. In it’s simplest form, it looks like this (remember, this goes into models.py
of your app, e.g. my_survey_app
):
# make sure to use a tuple instead of a list here, otherwise oTree will complain:
SURVEY_DEFINITIONS = (
{
'page_title': 'Survey Questions - Page 1',
'survey_fields': [
('age', {
'text': 'How old are you?',
'field': models.PositiveIntegerField(min=18, max=100),
}),
('occupation', {
'text': "What's your occupation?",
'field': models.CharField(blank=True),
}),
# ... more questions for this page
]
},
{
'page_title': 'Survey Questions - Page 2',
'survey_fields': [
('years_study', {
'text': 'How many years did you study in an University?',
'field': models.PositiveIntegerField(min=0, max=30),
}),
# ... more questions for this page
]
},
# ... more pages
)
We can see that each element in the SURVEY_DEFINITIONS
list is a dictionary which defines 1) the page’s title and 2) the questions that appear on that page. These questions are listed in survey_fields
as a 2-tuple consisting of 1) the field name which is the name of the variable for which the data will be collected; and 2) a dictionary defining the question (text
) and the actual input field (field
) to be used.
The first of the survey page definitions will be rendered like this:
You can add any type of field that oTree (or Django) supports (see the oTree documentation for a list of supported field types), e.g. boolean (yes/no) fields, currency fields, choices, etc.
otreeutils also adds functions that allow to create Likert scale inputs and Likert scale “matrices” (i.e. tables of Likert scale inputs) more easily. For example, to create a Likert five-point scale, we can use generate_likert_field
:
from otreeutils.surveys import generate_likert_field
likert_5_labels = (
'Strongly disagree', # value: 1
'Disagree', # value: 2
'Neither agree nor disagree', # ...
'Agree',
'Strongly agree' # value: 5
)
likert_5point_field = generate_likert_field(likert_5_labels)
likert_5point_field
is now a function to generate new fields of the specified Likert scale, which we can use in a survey definition:
# this is appended to SURVEY_DEFINITIONS from above:
{
'page_title': 'Survey Questions - Page 3',
'survey_fields': [
('q_otree_surveys', {
'help_text': """
<p>Consider this quote:</p>
<blockquote>
"oTree is great to make surveys, too."
</blockquote>
<p>What do you think?</p>
""",
'field': likert_5point_field(),
}),
('q_just_likert', {
'label': 'Another Likert scale input:',
'field': likert_5point_field(),
}),
]
},
# ... more pages
We produce several five-point Likert scale inputs by using likert_5point_field()
for each question. Please also note that you can add HTML as help_text
to each field. The output looks like this:
Let’s use generate_likert_table
to create a table of five-point Likert scale inputs, also added to the survey definition:
# this is appended to SURVEY_DEFINITIONS from above:
{
'page_title': 'Survey Questions - Page 4',
'survey_fields': [
generate_likert_table(likert_5_labels, [
('pizza_tasty', 'Tasty'),
('pizza_spicy', 'Spicy'),
('pizza_cold', 'Too cold'),
],
form_help_initial='<p>How was your latest Pizza?</p>'
)
]
}
This will create a table with five columns (the previously defined Likert five-point scale) and three rows (properties of the pizza). The rows are specified as 2-tuples of format ('my_field_name'
, 'My label')
An initial question can be added on top via form_help_initial
.
This survey page definition will be rendered like this:
We would now have a SURVEY_DEFINITIONS
data structure with several questions on four pages. We can now generate a Player
model class from the survey definitions using create_player_model_for_survey
. This dynamically created Player
model class will contain all the fields specified in the survey definitions. We also need to pass the module for which the Player
class will be created (the models
module inside your app, e.g. my_survey_app.models
):
# still in models.py:
from otreeutils.surveys import create_player_model_for_survey
Player = create_player_model_for_survey('my_survey_app.models',
SURVEY_DEFINITIONS)
Registering survey pages in pages.py
We still need to register the survey pages to oTree which must be done in pages.py
. We defined four pages before, so we need to create four page classes, each of them a sub-class of SurveyPage
:
# in pages.py:
from otreeutils.surveys import SurveyPage
class SurveyPage1(SurveyPage):
pass
class SurveyPage2(SurveyPage):
pass
class SurveyPage3(SurveyPage):
pass
class SurveyPage4(SurveyPage):
pass
You don’t need to implement anything else in these classes, everything is handled by otreeutils!2
We need to create a list of these pages and then set them up using setup_survey_pages
. The Player
class will be passed to all survey pages and the questions for each page will be set according to their order in the survey definitions.
from otreeutils.surveys import setup_survey_pages
survey_pages = [
SurveyPage1,
SurveyPage2,
SurveyPage3,
SurveyPage4
]
setup_survey_pages(Player, survey_pages) # Player from "models"
Finally, you set the page sequence of the app. Here, you may add some other pages before the questionnaire and some pages after it, e.g.:
page_sequence = [
SurveyIntro, # define some pages that come before the survey
# ...
]
# add the survey pages to the page sequence list
page_sequence.extend(survey_pages)
# add more pages that come after the survey:
page_sequence.extend([AnotherPage, ...])
That’s it! No need to further set up pages, write templates, etc. Whenever you need to change something regarding the survey, you only need to apply those changes at a single point, SURVEY_DEFINITIONS
. You may only need to add or remove SurveyPage
classes in pages.py
.
You can have a look at readily implemented examples in the otreeutils GitHub repository. Example 2 uses the survey tools and shows some more features.
- oTree: Implementing experiments with dynamically determined data quantity, M. Konrad – Journal of Behavioral and Experimental Finance 2018. ↩
- Because adding these “stump” pages seems redundant, I tried to implement the dynamic creation of those pages. However, I couldn’t achieve that oTree would except them. ↩
Recent Comments