Skip to content

Commit

Permalink
Merge branch 'treatment' of https://github.com/presleyp/Speriment
Browse files Browse the repository at this point in the history
Add ability to choose blocks on per-participant basis.
  • Loading branch information
presleyp committed Jan 9, 2015
2 parents 2d43acb + 6ce298c commit ee2e3d1
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 79 deletions.
101 changes: 85 additions & 16 deletions doc/example.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from speriment import * # get access to the module
##### Import the module ######
from speriment import *
# import * is generally bad practice, but Speriment scripts are unlikely to get
# complicated enough that you would have clashing variable names in your
# namespace. But, you can always do "import speriment" and use names like
# speriment.Block instead of Block.

##### IDs #######

# Every option, page, and block of your experiment needs an ID. There are two ways to get
# them:
# 1. Use this "with" statement to generate them magically, and indent ALL
Expand All @@ -13,6 +16,8 @@
# pass in other information, like: page1 = Page('hello', ID = row[0])
with make_experiment(IDGenerator()):

####### Read in your materials #######

# Imagine you have two csv files, items1.csv, which looks like this:

# What is your favorite animal?,cat,dog,animate-condition
Expand All @@ -30,6 +35,8 @@
# This is how to read in items2.csv
items2 = get_dicts('items2.csv')

###### Create experiment components. #####

# You can use a loop or list comprehension to make pages from your items.

# after using get_rows, access cells with indices, starting from 0
Expand All @@ -51,11 +58,34 @@

block1 = Block(pages = pages1)

###### Make blocks run conditionally (RunIf) #######

# I'll make the second block run only for those participants who answer "cat" to
# the animal question in block1
animal_question = pages1[0]
cat_condition = RunIf(page = animal_question, option = animal_question.options[0])
block2 = Block(pages = pages2, run_if = cat_condition)
conditional_block = Block(pages = pages2, run_if = cat_condition)

###### Make a Latin Square (groups, latin_square) #####

# Normally blocks have pages. Latin square blocks have groups, which are
# lists of pages.

page_1a = Page("1A")
page_1b = Page("1B")
page_2a = Page("2A")
page_2b = Page("2B")

latin_square_block = Block(groups = [[page_1a, page_1b], [page_2a,
page_2b]], latin_square = True)

# Now, half your participants will see page_1a and page_2b, and half will
# see page_1b and page_2a. The order of the pages will be shuffled as usual.

# If you use groups and latin_square is False, pages will be chosen from
# them randomly rather than according to a Latin Square.

###### Create components differently across participants (SampleFrom) ######

# Let's make a block where the texts of pages are combined with the other
# data for the page differently across participants. We need to replace the text string
Expand All @@ -68,48 +98,87 @@

sampled_pages = [Page(SampleFrom('bank1'), condition = row['Condition'])
for row in items2]
block3 = Block(pages = sampled_pages, banks = {'bank1': ['sampled1',
sampling_block = Block(pages = sampled_pages, banks = {'bank1': ['sampled1',
'sampled2']})

####### Copy and tweak components without messing up IDs (new) ######

# Now I want to make another block just like block 1, and then just tweak it
# a little bit.
# The "new" method ensures they get separate IDs, which can be important for how
# the experiment runs. Do this whenever you copy an option, page, or block
# if you're using the "with" statement.

block4 = block1.new()
copied_block = block1.new()

# I just want block4 to have one more page. This page doesn't have options,
# which is fine; it'll just show some text.

block4.pages.append(Page('This is almost the last block.'))
copied_block.pages.append(Page('This is an extra page.'))

####### Change order or choice of blocks across participants (exchangeable,
####### counterbalance, treatments) #####

# So now block1 and copied_block are mostly the same. Maybe we want to show
# half the participants block1 and half copied_block, like this:

block_of_blocks = Block(blocks = [block1, copied_block], treatments =
[[block1], [copied_block]])

# This means that in treatment 1, block1 will show, and in treatment 2,
# copied_block will show. We could have put other blocks in these
# treatments, or had blocks in block_of_blocks that aren't in any treatments
# and thus show for all participants.

# They don't have to be adjacent or in the same larger block for this to work - we could
# have put the treatments argument on the entire Experiment.

# The same rules apply to the exchangeable argument and the counterbalance
# argument. If you want two or more blocks to switch places with each other
# across participants, you can do:

alternative_block_of_blocks = Block(blocks = [block1, copied_block],
counterbalance = [block1, copied_block])

# or the same with exchangeable instead of counterbalance. Exchangeable
# decides the order randomly. Counterbalance decides deterministically, so
# you'll get a more even distribution across participants. Counterbalance
# and treatments use the same variable to make decisions, so you probably
# don't want to use them in the same experiment. This variable is based on
# num_counters in config.txt, so make sure to set it if you use
# counterbalance or treatments.

# Note that if we use block_of_blocks in the experiment, the animal_question
# that conditional_block depends on might not ever show! The copied version
# of it is not the same as the original. If it doesn't show, then
# conditional_block will not show either.

####### Control the order of pages via blocks #######

# That page will occur somewhere in block 4, but we don't know exactly where.
# Blocks stay put unless they're exchangeable, but questions move around in
# their blocks. Here's a block with just one page so we know it'll come last.

block5 = Block(pages = [Page('Goodbye!')])
last_block = Block(pages = [Page('Goodbye!')])

###### Make an Experiment ######

# Finally, wrap the Blocks in an Experiment. Remember that Pages take an
# optional list of Options, Blocks take a list of Pages (or a list of lists of
# Pages, or a list of Blocks), and Experiments take a list of Blocks.
# The counterbalance argument says that block1 and block4 will switch places
# for approximately half of participants. It needs to be used in conjunction
# with setting the counterbalance parameter in PsiTurk's config.txt,
# whereas the exchangeable argument could be used in the same way but
# without setting that parameter (but it will accordingly give a less even
# distribution across participants).

experiment = Experiment([block1, block2, block3, block4, block5], counterbalance =
[block1, block4])
experiment = Experiment([block_of_blocks,
#block1,
latin_square_block, conditional_block, sampling_block,
last_block])

# You can generate the JSON just to look at it, for instance by printing this
# variable. This step is optional.

exp_json = experiment.to_JSON()

# Finally, run this line to make sure your experiment is written properly,
# convert it to JSON, write it to a file, and tell PsiTurk where to find
# This line checks that your experiment is written properly,
# converts it to JSON, writes it to a file, and tells PsiTurk where to find
# Speriment and your JSON. Just make up a name for this experiment, which
# will be used to name the JSON object and the JavaScript file it's stored
# in. Make sure to run this script in the top level of your PsiTurk project
Expand Down
94 changes: 66 additions & 28 deletions speriment/speriment.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,29 @@ def rename_key(self, dictionary, key, new_key):
else:
return dictionary

def compile_treatments(self, obj):
'''Treatments are lists of lists of blocks to run conditionally. Remove
this variable and add RunIf objects to those blocks.'''
for i, treatment in enumerate(obj.treatments):
for block in treatment:
block.run_if = RunIf(permutation = i)
del obj.treatments
return obj

def default(self, obj):
if isinstance(obj, Component):
if hasattr(obj, 'treatments'):
obj = self.compile_treatments(obj)
obj.validate()
dict_copy = copy.deepcopy(obj.__dict__)
# make keys follow JS conventions
renamed_ls = self.rename_key(obj.__dict__, 'latin_square', 'latinSquare')
renamed_ri = self.rename_key(renamed_ls, 'run_if', 'runIf')
renamed_id = self.rename_key(renamed_ls, 'id_str', 'id')
renamed_id = self.rename_key(renamed_ri, 'id_str', 'id')
return renamed_id
if isinstance(obj, RunIf):
return obj.__dict__
renamed_option = self.rename_key(obj.__dict__, 'option_id', 'optionID')
renamed_page = self.rename_key(renamed_option, 'page_id', 'pageID')
return renamed_page
if isinstance(obj, SampleFrom):
return {"sampleFrom": obj.bank}
# Let the base class default method raise the TypeError
Expand Down Expand Up @@ -122,7 +134,8 @@ def __exit__(self, etype, evalue, etrace):
### Special kind of experimental components (not in the hierarchy)

class RunIf:
def __init__(self, page, option = None, regex = None):
def __init__(self, page = None, option = None, regex = None, permutation =
None):
'''
page: Page, the Page to look at to see which answer was given.
Expand All @@ -133,16 +146,22 @@ def __init__(self, page, option = None, regex = None):
only run if a response matching a regular expression made from the
string regex was given the last time page was displayed.
Exactly one of option and regex must be given.
permutation: integer, optional. If given, the block containing this
RunIf will only run if PsiTurk gives the experiment a permutation
variable matching this integer. This requires editing the counterbalance
setting in config.txt.
The reason RunIfs depend on "the last time" their page was displayed is
that Pages can display multiple times if they are in Blocks with a
criterion.'''
self.page_id = page.id_str
if page != None:
self.page_id = page.id_str
if option != None:
self.option_id = option.id_str
elif regex != None:
self.regex = regex
if permutation != None:
self.permutation = permutation

class SampleFrom:
def __init__(self, bank):
Expand Down Expand Up @@ -302,7 +321,7 @@ def validate(self):

class Block(Component):
def __init__(self, pages = None, groups = None, blocks = None, id_str = None,
exchangeable = [], counterbalance = [], latin_square = None, pseudorandom = None, **kwargs):
exchangeable = [], counterbalance = [], treatments = [], latin_square = None, pseudorandom = None, **kwargs):
'''
Exactly one of pages, groups, and blocks must be provided.
Expand Down Expand Up @@ -400,12 +419,29 @@ def __init__(self, pages = None, groups = None, blocks = None, id_str = None,

if pseudorandom:
self.pseudorandom = pseudorandom
self.validate_pseudorandom()

# TODO what about mutated blocks
if treatments:
self.treatments = treatments
# for (i, treatment) in enumerate(treatments):
# for block in treatment:
# block.run_if = RunIf(permutation = i)

def validate(self):
self.validate_contents()
self.validate_pseudorandom()
self.validate_latin_square()
self.validate_counterbalancing()

def validate_counterbalancing(self):
if hasattr(self, 'counterbalance') and hasattr(self, 'treatments'):
print '''Warning: counterbalance and treatments depend on the same
variable, so using both in one experiment will cause
correlations between which blocks are used and how blocks are
ordered. If you want these to be decided independently, change
'counterbalance' to 'exchangeable' so the order will be decided
randomly for each participant.'''


def validate_contents(self):
content_types = [attribute for attribute in
Expand All @@ -415,25 +451,26 @@ def validate_contents(self):
and blocks.'''

def validate_pseudorandom(self):
if hasattr(self, 'groups'):
if self.latin_square == False:
raise ValueError, '''Can't choose pages from groups randomly and
ensure that pseudorandomization will work. Supply pages instead of
groups, change latin_square to True, or change pseudorandom to
False.'''
try:
conditions = [page.condition for group in self.groups for page in
group]
except AttributeError:
raise ValueError, '''Can't pseudorandomize pages without
conditions.'''
cond_counter = Counter(conditions)
cond_counts = cond_counter.values()
num_cond_counts = len(set(cond_counts))
if num_cond_counts != 1:
raise ValueError, '''Can't pseudorandomize pages if not all
conditions are represented the same number of times in the
block.'''
if hasattr(self, 'pseudorandom'):
if hasattr(self, 'groups'):
if self.latin_square == False:
raise ValueError, '''Can't choose pages from groups randomly and
ensure that pseudorandomization will work. Supply pages instead of
groups, change latin_square to True, or change pseudorandom to
False.'''
try:
conditions = [page.condition for group in self.groups for page in
group]
except AttributeError:
raise ValueError, '''In block {0}, can't pseudorandomize pages without
conditions.'''.format(self.id_str)
cond_counter = Counter(conditions)
cond_counts = cond_counter.values()
num_cond_counts = len(set(cond_counts))
if num_cond_counts != 1:
raise ValueError, '''Can't pseudorandomize pages if not all
conditions are represented the same number of times in the
block.'''
#TODO elif hasattr('pages')

def validate_latin_square(self):
Expand Down Expand Up @@ -465,7 +502,8 @@ def __init__(self, blocks, exchangeable = [], counterbalance = [], banks =
self.blocks = [b for b in blocks]
if exchangeable:
self.exchangeable = [b.id_str for b in exchangeable]
self.counterbalance = [b.id_str for b in counterbalance]
if counterbalance:
self.counterbalance = [b.id_str for b in counterbalance]
if banks:
self.banks = banks

Expand Down
8 changes: 8 additions & 0 deletions speriment/sperimentschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@
}
},
"required": ["pageID"]
},
{
"properties": {
"permutation": {
"type": "number"
}
},
"required": ["permutation"]
}
]
},
Expand Down
15 changes: 14 additions & 1 deletion speriment/test/test_speriment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from speriment import *
import json

def test_new():
with make_experiment(IDGenerator()):
Expand All @@ -20,4 +21,16 @@ def test_get_dicts():
rows2 = get_dicts('speriment/test/comma_sep.csv')
assert rows2 == answer


def test_compile_treatments():
with make_experiment(IDGenerator()):
b1 = Block(pages = [])
b2 = Block(pages = [])
b3 = Block(pages = [])
outer = Block(blocks = [b1, b2, b3], treatments = [[b1, b3], [b2]])
exp = Experiment(blocks = [outer])
json_exp = exp.to_JSON()
compiled_exp = json.loads(json_exp)
print compiled_exp
assert compiled_exp['blocks'][0]['blocks'][0]['runIf']['permutation'] == 0
assert compiled_exp['blocks'][0]['blocks'][1]['runIf']['permutation'] == 1
assert compiled_exp['blocks'][0]['blocks'][2]['runIf']['permutation'] == 0

0 comments on commit ee2e3d1

Please sign in to comment.