3. Control Flow#

Programs are shaped by control flow, which determines which commands run at which times. Python programs frequently rely on a few key mechanisms of control flow.

3.1. For loops#

A common pattern in programming is running some commands multiple times using a loop.

In Python, we can do this using a for loop:

for item in items:
    some_code_to_run_for_each_item

Sometimes, we have a list of data and need to do something with each item in the list. We can use a for loop to access one item at a time.

words = ["piano", "fountain", "hollow", "pupil"]
for word in words:
    print(word)
piano
fountain
hollow
pupil

Here, word is a new kind of variable we haven’t seen yet. It changes its value at each step of the loop. First, it’s "piano", then "fountain", etc., until the end of the list of words. Each time we run print(word), we’re printing a different word in the list.

Let’s take another look at what happens during a for loop. If we run the following loop:

numbers = [1, 2, 3]
for number in numbers:
    print(number)

Each step of the loop accesses a different part of the list:

numbers = [1, 2, 3]
number  =  ^

number = 1

numbers = [1, 2, 3]
number  =     ^

number = 2

numbers = [1, 2, 3]
number  =        ^

number = 3

Exercise: for loops#

Create a list with the following participant IDs: sub-001, sub-002, and sub-004. Write a for loop over your list and use print to display each item in the list. Choose informative names for your list of participant IDs and the current participant ID.

# answer here

3.2. Modifying a list#

We can use for loops for lots of things. One common pattern is to loop over every item in a list and create a new list by modifying that list.

Say we want to use the capitalize method of strings to convert a list of words from lower case to capitalized case.

First, we can test how this works using an example word. If we have the word stored in a string variable called word, we can use the capitalize method to create a capitalized version of the word.

word = "piano"
capitalized_word = word.capitalize()
print(word, capitalized_word)
piano Piano

If we want to capitalize each word in the words list, we can do this by either writing multiple, very similar commands, or using a for loop.

We can capitalize all the words by running the capitalize method on each word in the list manually.

cap = []  # empty list that will hold all the capitalized words
cap.append(words[0].capitalize())  # capitalize the first word and add to the list
cap.append(words[1].capitalize())  # second word
cap.append(words[2].capitalize())  # third word
cap.append(words[3].capitalize())  # fourth word
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

Notice that this involves a lot of code, and we need to add another line for each item in the list. Let’s try using a for loop instead.

cap = []
for word in words:
    cap.append(word.capitalize())
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

Now, we only have to write the cap.append(word.capitalize()) part once, and let the for loop handle the iterating part.

Let’s take another look at building a list using a for loop. If we run the following loop:

letters = ["a", "b"]
cap = []
for letter in letters:
    cap.append(letter.capitalize())

Before the loop, cap is empty:

cap = []

On each step of the loop, we access a letter, capitalize it, and append it to the cap list:

letters = ["a", "b"]
letter  =   ^

cap = ["A"]

letters = ["a", "b"]
letter  =        ^

cap = ["A", "B"]

Exercise: modifying a list#

Say you have a list of scores on a memory test and need to convert them to percentages. Use a for loop to iterate over each score and create a new list called percentages. For each element of the list, the percentages list should contain the score divided by 8 to turn it into a fraction, then multiplied by 100 and rounded to the nearest integer to get a percentage.

You can either make the percentage calculation in one expression or create variables to store intermediate values. For example, in your loop, you could create a variable called fraction, then use that to create a variable called percentage, and finally append that to the percentages list.

scores = [4, 8, 5, 2, 6, 3, 7, 4]
# answer here

3.3. List comprehensions#

In Python, list comprehensions allow us to run some for loops in one line, when we just want to generate a new list.

Before, we used a for loop to capitalize all the items in a list.

words = ["piano", "fountain", "hollow", "pupil"]
cap = []
for word in words:
    cap.append(word.capitalize())
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

Python allows us to write the same thing in one line, using a list comprehension.

cap = [word.capitalize() for word in words]
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

The first part (word.capitalize()) indicates how to define each item in the new list. The second part (for word in words) indicates what we’re looping over, and what each item in the loop should be called. Here, we set each word to the temporary variable word.

We can also add an if statement at the end to filter the input list. Only items that return True will be included.

cap_p = [word.capitalize() for word in words if word.startswith("p")]
print(cap_p)
['Piano', 'Pupil']

List comprehensions take a little getting used to, but can be useful to express list creation succinctly. You can always write out a for loop instead though!

Exercise: list comprehensions#

The following code uses a for loop to take a list of participant numbers and translate them into participant identifier strings.

numbers = [1, 2, 4, 5, 7, 9, 11, 12]

participant_ids = []
for number in numbers:
    participant_ids.append(f"sub-{number:02d}")

Use a list comprehension to generate the list of participant identifiers in one line of code.

numbers = [1, 2, 4, 5, 7, 9, 11, 12]
# answer here

3.4. Using counters with loops#

When looping over a list, we may have some count that we want to keep track of. We can keep a count using a variable that changes on each iteration of a loop.

For example, say that we have a list with scores on a series of memory tests, and we want to calculate the total.

test_scores = [20, 24, 23, 25]

We can calculate the total using a for loop with a counter variable. To calculate a sum, we initialize our counter to 0 and then add to it on each iteration of the loop.

total = 0
for score in test_scores:
    total += score
print(total)
92

The += operator makes it easier to increment a variable. total += score means the same thing as total = total + score. On each iteration of the loop, we add the current score to the total stored in total.

Exercise: using counters with loops#

Say there were a series of trials where a participant had to say whether a target stimulus was present or absent. There were ten trials, which were coded as either correct (1) or incorrect (0).

Given the accuracy list defined below, use a for loop with a counter variable to calculate the total number of correct trials. Finally, divide the number of correct trials by the total number of trials to calculate the fraction of correct trials.

accuracy = [0, 0, 1, 1, 1, 0, 1, 0, 1, 1]
# answer here

3.5. Looping over multiple lists#

So far, we have looped over items in a list. Sometimes, we may have related data in multiple lists that we want to analyze together.

For example, say that we have one list with participant IDs and another list with the corresponding ages of the participants.

participant_id = ["001", "002", "003"]
age = [23, 34, 18]

We can loop over multiple lists simultaneously using the zip function, which “zips” together two (or more) lists so you can iterate over them together. At each part of the list, you get a tuple of the current element of the two lists.

for p_id, p_age in zip(participant_id, age):
    print(p_id, p_age)
001 23
002 34
003 18

Let’s look at looping using zip in more detail. If we run the following loop:

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
for n1, n2 in zip(numbers1, numbers2):
    print(n1, n2)

Each step of the loop accesses pairs of the items in the list:

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
n1, n2   =  ^

n1 = 1, n2 = 4

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
n1, n2   =     ^

n1 = 2, n2 = 5

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
n1, n2   =        ^

n1 = 3, n2 = 6

Exercise: looping over multiple lists#

Say you have three lists providing the participant ID, condition, and percentage correct for a set of participants. Use a for loop with zip to print a summary of each participant and their performance. For example, if participant ID was “sub-001”, condition was “restudy”, and percentage correct was 75, print “sub-001 (restudy): 75%”. Your code should display a summary in that format for each of the participants.

participant_ids = ["sub-001", "sub-002", "sub-003", "sub-004"]
conditions = ["restudy", "test", "restudy", "test"]
percentages = [75, 82, 63, 87]
# answer here

3.6. Using conditionals with loops#

We can use if statements within loops to create more flexible programs.

Say we have a set of participants, their ages, and their scores from a memory test.

participant_ids = ["sub-001", "sub-002", "sub-003", "sub-004"]
ages = [19, 22, 31, 27]    # age of participants in years
scores = [82, 89, 65, 98]  # memory test score out of 100

If we want to calculate the mean score, we can do this using a for loop with a variable that keeps track of the total, which is then divided by the number of participants. The total_score is initialized to 0, but then increases on each loop by adding the current score. We can use total_score += score to add score to the current value of total_score.

total_score = 0
for score in scores:
    total_score += score  # add this score to the total
mean_score = total_score / len(scores)
print(mean_score)
83.5

Often, however, we may want to look at subsets of a dataset. For example, we might want to get the mean score for participants under 30 years. We can do this by combining a for loop with an if statement. Note that we need to both keep track of the total score (total_score) and the number of included participants (n_included).

total_score = 0  # total score so far
n_included = 0   # number of included participants
for age, score in zip(ages, scores):
    if age < 30:
        total_score += score  # add this score to the total
        n_included += 1       # add one to the count
mean_score_included = total_score / n_included
print(mean_score_included)
89.66666666666667

The score for under-30 participants (89.7) was higher compared to when all participants were included (83.5).

Exercise: using conditionals with for loops#

Say you have measured accuracy on four different cognitive tasks. You want to calculate the mean accuracy across different tasks, but only including some of the tasks. The accuracy variable indicates the accuracy on each of the four tasks, and the included variable indicates whether each of the four tasks should be included (if it is True) or excluded (if it is False).

Use a for loop with an if statement to calculate the mean accuracy for the included scores only.

accuracy = [0.85, 0.92, 0.25, 0.87]
included = [True, True, False, True]
# answer here

3.7. Exceptions#

Sometimes a program is unable to run, often because of some problem with the inputs to that function. To deal with these cases, they can signal a problem by raising an exception.

When an exception is raised, it will halt execution of the program. This allows Python programs to stop if some assumption of the program has been violated.

For example, say we need to check that participant IDs are formatted correctly. We can use the .startswith string method to check if it starts with the correct "sub-" text. If not, then we raise an error.

participant_ids = ["sub-001", "sub-002", "sub-003"]
# participant_ids.append("subs-004")  # throws an error (try it!)
for pid in participant_ids:
    if not pid.startswith("sub-"):
        raise ValueError(f"ID does not start with 'sub-': {pid}")

There are many kinds of errors to signal different types of things going wrong. Two common ones are ValueError (there was some sort of unexpected value for a variable), TypeError (a variable had the wrong data type), and RuntimeError (there was some other unexpected problem).

We can let a program continue even if an exception is raised by using a try/except block.

letters = ["a", "b", 2]
cap = []
for letter in letters:
    try:
        cap.append(letter.capitalize())
    except:
        continue
print(cap)
['A', 'B']

This lets us “try” some code, see if there is an exception, and if so, run some “backup” code in case an exception has been raised. Here, we just use the continue keyword to skip to the next loop.

Exercise: exceptions#

Create a variable x = ["a", 1]. Try running print(sum(x)). This will raise an exception because you can’t sum a letter and a number together. Write a try/except block that attempts to run print(sum(x)) and prints "Error: cannot calculate sum" if an exception is raised.

Verify that you see your error message when x = ["a", 1]. Try changing x to only include numbers, and verify that the sum is calculated without printing an error message.

# answer here

3.8. Programming simple analyses#

By putting functions, loops, if statements, and exceptions together, we can start to create simple analysis programs.

In this section, we will use the programming tools we have learned so far to analyze a simple example dataset. Say we have data from a participant who completed a series of perceptual judgments to indicate whether a display with dots appeared to be moving left or right.

dot motion high coherence dot motion low coherence

The first display shows higher coherence motion to the right. The second display shows lower coherence motion to the left. These examples are captured from an online implementation of a dot-motion task by Jordan Deakin.

The direction measure indicates which direction the dots tended to move (left or right).

direction = [
    "left", 
    "left", 
    "right", 
    "right", 
    "right", 
    "left", 
    "right", 
    "right", 
    "left", 
    "left", 
    "left", 
    "right",
]

The coherence measure indicates how coherent the movement of the dots was (low, medium, or high).

coherence = [
    "low",
    "high",
    "medium",
    "high",
    "low",
    "medium",
    "high",
    "low",
    "medium",
    "low",
    "high",
    "medium",
]

The response measure indicates how the participant responded (left or right).

response = [
    "right",
    "left",
    "right",
    "right",
    "right",
    "right",
    "right",
    "left",
    "left",
    "left",
    "left",
    "right",
]

Exercise: calculating a total#

First, let’s look at whether this participant was more likely to respond left or right overall. The actual direction was balanced (that is, movement was to the left or right on an equal number of trials), so if the participant tended to respond one direction more often, they might have been biased toward making that response.

To do this, we need the total number of “left” responses, divided by the total number of trials. If the fraction of left responses is very high, we might consider whether the participant was biased to respond “left”, while if it is very low, we might consider whether the participant was biased to respond “right”.

What fraction of responses were “left”? Does it seem like their responses were biased to one direction or another? How can you tell?

print(response)
['right', 'left', 'right', 'right', 'right', 'right', 'right', 'left', 'left', 'left', 'left', 'right']
# answer here

Exercise: calculating overall accuracy#

Next, let’s calculate the overall accuracy. We want to know on what fraction of trials the participant responded with the correct direction.

To do this, we need the number of correct trials (where the response was the same as the direction) and the total number of trials. Dividing the number of correct trials by the total number of trials will give accuracy, measured as the fraction of correct trials.

What was the overall accuracy? Does it seem like accuracy was greater than you would expect if they were just guessing? How can you tell?

Advanced: summarize performance#

Use an f-string to create a summary of performance showing the number of correct trials out of the number of trials and the percentage accuracy rounded to the nearest percent. For example, if someone got 6 out of 12 trials correct, their performance summary would be "6/12 trials (50% correct)".

print(direction)
print(response)
['left', 'left', 'right', 'right', 'right', 'left', 'right', 'right', 'left', 'left', 'left', 'right']
['right', 'left', 'right', 'right', 'right', 'right', 'right', 'left', 'left', 'left', 'left', 'right']
# answer here

Exercise: calculating accuracy for a specific condition#

Finally, let’s examine accuracy in the different coherence conditions. We want to measure accuracy in each condition, and that will involve similar code for each condition. Let’s create a function that can analyze any one condition, and then use that to examine each condition in turn.

Write a function called accuracy_coherence that takes the direction, coherence, and response variables, along with an include_coherence variable that indicates which level of coherence ("low", "medium", or "high") to analyze. Your function should return the mean accuracy for the coherence level specified by the include_coherence input.

Use your function to calculate accuracy in each coherence condition. Does it seem like accuracy varies with coherence? How can you tell?

print(direction)
print(coherence)
print(response)
['left', 'left', 'right', 'right', 'right', 'left', 'right', 'right', 'left', 'left', 'left', 'right']
['low', 'high', 'medium', 'high', 'low', 'medium', 'high', 'low', 'medium', 'low', 'high', 'medium']
['right', 'left', 'right', 'right', 'right', 'right', 'right', 'left', 'left', 'left', 'left', 'right']
# answer here

3.9. Summary#

Like all programming languages, Python has control flow commands determine what code to run at what times.

if/elif/else statements run different code depending on what conditions apply.

for loops iterate over elements and run some code for each element.

exceptions allow programs to quit if there is some problem that keeps them from being able to do their job.

Combining these elements makes it possible to create programs to analyze data.