Iteration

It is often the case in programming – especially when dealing with randomness – that we want to repeat a process multiple times. For example, we might want to assign each person in a study to the treatment group or to control, based on tossing a coin. We can do this without actually tossing a coin for each person; we can just use np.random.choice instead.

Here is a reminder of how np.random.choice works. Run the cell a few times to see how the output changes.

np.random.choice(make_array('Heads', 'Tails'))
'Heads'

To come up with Heads or Tails for each individual in our study, we could copy-paste the code multiple times, but that’s tedious and prone to typos, and if we wanted to do it a thousand times or a million times, forget it.

A more automated solution is to use a for statement to loop over the contents of a sequence. This is called iteration. A for statement begins with the word for, followed by a name we want to give each item in the sequence, followed by the word in, and ending with an expression that evaluates to a sequence. The indented body of the for statement is executed once for each item in that sequence.

for i in np.arange(3):
    print(i)
0
1
2

It is instructive to imagine code that exactly replicates a for statement without the for statement. This is called unrolling the loop.

A for statement simple replicates the code inside it, but before each iteration, it assigns a new value from the given sequence to the name we chose. For example, here is an unrolled version of the loop above:

i = np.arange(3).item(0)
print(i)
i = np.arange(3).item(1)
print(i)
i = np.arange(3).item(2)
print(i)
0
1
2

Notice that the name i is arbitrary, just like any name we assign with =.

Here we use a for statement in a more realistic way: we print 5 random choices from coin, thus simulating the results five tosses of a coin. We use the word simulating to remind ourselves that we are not physically tossing coins but using Python to mimic the process.

coin = make_array('Heads', 'Tails')

for i in np.arange(5):
    print(np.random.choice(coin))
Heads
Heads
Heads
Tails
Heads

In this case, we simply perform exactly the same (random) action several times, so the code inside our for statement does not actually refer to i.

Augmenting Arrays

While the for statement above does simulate the results of five tosses of a coin, the results are simply printed and aren’t in a form that we can use for computation. Thus a typical use of a for statement is to create an array of results, by augmenting it each time.

The append method in numpy helps us do this. The call np.append(array_name, value) evaluates to a new array that is array_name augmented by value. When you use append, keep in mind that all the entries of an array must have the same type.

pets = make_array('Cat', 'Dog')
np.append(pets, 'Another Pet')
array(['Cat', 'Dog', 'Another Pet'], dtype='<U11')

This keeps the array pets unchanged:

pets
array(['Cat', 'Dog'], dtype='<U3')

But often while using for loops it will be convenient to mutate an array – that is, change it – when augmenting it. This is done by assigning the augmented array to the same name as the original.

pets = np.append(pets, 'Another Pet')
pets
array(['Cat', 'Dog', 'Another Pet'], dtype='<U11')

Example: Counting the Number of Heads

We can now simulate five tosses of a coin and place the results into an array. We will start by creating an empty array and then appending the outcome of each toss. Notice that the body of the for loop contains two statements. Both statements are executed for each value in the given sequence np.arange(5).

coin = make_array('Heads', 'Tails')

outcomes = make_array()

for i in np.arange(5):
    outcome_of_toss = np.random.choice(coin)
    outcomes = np.append(outcomes, outcome_of_toss)
    
outcomes
array(['Tails', 'Tails', 'Tails', 'Heads', 'Tails'], dtype='<U32')

Let us rewrite the cell with the for statement unrolled:

coin = make_array('Heads', 'Tails')

outcomes = make_array()

i = np.arange(5).item(0)
outcome_of_toss = np.random.choice(coin)
outcomes = np.append(outcomes, outcome_of_toss)

i = np.arange(5).item(1)
outcome_of_toss = np.random.choice(coin)
outcomes = np.append(outcomes, outcome_of_toss)

i = np.arange(5).item(2)
outcome_of_toss = np.random.choice(coin)
outcomes = np.append(outcomes, outcome_of_toss)

i = np.arange(5).item(3)
outcome_of_toss = np.random.choice(coin)
outcomes = np.append(outcomes, outcome_of_toss)

i = np.arange(5).item(4)
outcome_of_toss = np.random.choice(coin)
outcomes = np.append(outcomes, outcome_of_toss)

outcomes
array(['Heads', 'Heads', 'Heads', 'Tails', 'Heads'], dtype='<U32')

By capturing the results in an array we have given ourselves the ability to use array methods to do computations. For example, we can use np.count_nonzero to count the number of heads in the five tosses.

np.count_nonzero(outcomes == 'Heads')
4

Keep in mind that we have used the for loop to simulate a random experiment, and therefore if you run the cell again, the array outcomes is likely to be different. In upcoming sections of the course we will study how different the outcomes could be.

Iteration is a powerful technique. For example, by running exactly the same code for 1000 tosses instead of 5, we can count the number of heads in 1000 tosses.

outcomes = make_array()

for i in np.arange(1000):
    outcome_of_toss = np.random.choice(coin)
    outcomes = np.append(outcomes, outcome_of_toss)

np.count_nonzero(outcomes == 'Heads')
515