Loops#

Spike detection with python#

  • Present data in code (individual voltage values, manipulate them and store the results) - variables

  • Compare variables (voltage to threshold) - boolean values

  • Perform different actions based on the value of a variable (only keep the position if the voltage exceeds the threshold) - if-else statements

  • Present and access data in a time series of voltage values - lists

  • Perform an action for each element in a sequence of values (inspect voltage values one-by-one) - for loops

  • Separate data and logic so we can use the same code for new recordings - functions

  • Apply this to multi data files

  • Plot and save the results

Warm-up#

To warm up, let’s do a little exercise.

Here is a list: my_list = [10, 20, 30, 40]

Write code that divides each value in this list by 2 and prints the computed value.

my_list = [10, 20, 30, 40]
# your solution here

“For” loops#

Okay, lists are great - they can hold many values, we can access these values via indices, and we can check whether these values correspond to spikes.

How do we apply our logic to all values in the list automatically?

Using a “for” loop:

for item in list:
    indented block

The for loop automatically goes over elements of a list and allows us to apply the same computation to every element, like printing it:

my_list = [10, 20, 30, 40]

for number in my_list:
    half = number / 2
    print("inside the loop:", number, half)

print("after the loop", number, half)
inside the loop: 10 5.0
inside the loop: 20 10.0
inside the loop: 30 15.0
inside the loop: 40 20.0
after the loop 40 20.0

“Going over a list” or “looping over list” is also often called “iterating”.

What’s going on here?

  • we define a variable called my_list as a list of 4 numbers: 10, 20, 30, and 40

  • for number in my_list does the following:

    • it creates a variable called number (the loop variable). the loop variable can have any name.

    • the for loop goes through the list, element by element and in each step (“for each element”) it does the following:

      • set the value of the number variable to the current element

      • execute the indented block (in this case print the value of number)

    • repeat this until the end of the list is reached

We can “unroll” the for loop above - it corresponds to these individual steps of computation:

my_list = [10, 20, 30, 40]

number = my_list[0]
print(number)
number = my_list[1]
print(number)
number = my_list[2]
print(number)
number = my_list[3]
print(number)

Clicker question “for loops 1”(Click me!)

What is the output of this code?

my_list = [1,4,12]
for number in my_list:
    print(number - 1)

Bonus: Nested loops#

For loops can be nested - indentation matters:

for outer in [10, 20, 30]:
    print('outer', outer)
    for inner in ['A', 'B', 'C']:
        print('   inner', outer, inner)
outer 10
   inner 10 A
   inner 10 B
   inner 10 C
outer 20
   inner 20 A
   inner 20 B
   inner 20 C
outer 30
   inner 30 A
   inner 30 B
   inner 30 C

Sidenote: Because for and in are reserved words to run for loops, they are not valid variable names. Trying to create variables with those names, like for = 10 or in = "yes", will produce an error!!

Common code patterns#

Building lists#

So far we were able to manipulate list elements and print the result, but we did not have a way of saving the manipulated elements in a new list. You can do that by appending the results to a new empty list in the for loop:

# Goal: divide by ten each element in the list "data", collect the results in a new list "new_data"
data = [10, 20, 30]
print('data:', data)

# create an empty list that we will append the results to
new_data = []

# loop over all elements in data
for item in data:
    result = item / 10  # divide each list element by 10
    new_data.append(result)  # append the result to the `new_data` list

print('results:', new_data)
data: [10, 20, 30]
results: [1.0, 2.0, 3.0]

Important: append modifies the list in-place, it does not return a new list. This DOES NOT WORK: a = b.append(c)

Filtering lists#

Append the results to a new list using a for loop if they match a criterion:

numbers = [1, 15, 3, 2, 11, 7, 20]
print('numbers:', numbers)

# create an empty list that we will append the results to
results = []

# loop over all elements in numbers
for number in numbers:
    if number < 10:  # if the current number is <10
        results.append(number)  # add the number to our results list
print(results)
numbers: [1, 15, 3, 2, 11, 7, 20]
[1, 3, 2, 7]

Clicker question “filtering”(Click me!)

What is the content of big_numbers at the end of this code?

all_numbers = [1,12,4,19,28]
big_numbers = []

for number in my_list:
    if number > 10:
        big_numbers.append(number)
print(big_numbers)

Different ways of looping over lists#

Directly over the elements (preferred)#

So far, we have looped directly over list elements:

names = ['Tom', 'Yolanda', 'Estelle']
for name in names:  # loop directly over the names
    print(name)
Tom
Yolanda
Estelle

Using indices (but you really should loop directly)#

We can also generate a list of indices and use the indices to get the individual list elements. This is useful if we need to know the position of list elements matching a condition (e.g. for spike sorting).

names = ['Tom', 'Yolanda', 'Estelle']
indices = [0, 1, 2]
for index in indices:  # loop over the indices
    name = names[index]  # get the current name using the index
    print(index, name)
0 Tom
1 Yolanda
2 Estelle

Above, we have manually created a list of indices for looping. This is not practical for longer lists. The range function can be used to generate lists of indices.

The range function generates a sequence of integer numbers with specified start, stop (non-inclusive), and step (interval) value:

range(start, stop, step)

There are short cuts with implicit start=0 and step=1 values (similar to how slices work):

range(start, stop)  # integers from start to stop, in steps of 1
range(stop)  # integers from 0 to stop, in steps of 1

Note, that range does not return a list of indices but a special range object that generates the index as requested. That way we can have ranges that do not fit in memory, e.g. range(1000000000000000). Ranges are used in for loops. We can turn a range into list to learn how the work:

a = [1, 2, 3]
a[:2]

r = range(10)
print(r, type(r))
print(list(r))  # to inspect the range, we can cast it to a list. Note that the stop value (10) is not included (just like in slices)
print(list(range(0, 10, 1)))  # equivalent to range(10), the rest (start=0, step=1) is implicit

print(list(range(5, 10)))  # specify only start and stop, step=1 is implicit
print(list(range(5, 10, 2)))
range(0, 10) <class 'range'>
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[5, 6, 7, 8, 9]
[5, 7, 9]

Clicker question “range 1”(Click me!)

What list does list(range(3)) generate?

Clicker question “range 2”(Click me!)

Which range statement can be used to create the list [1, 3, 5]?

To generate a range for looping over a list, we provide the length of the list (len(my_list) as an argument to range: range(len(my_list)). This will generate a range starting at 0 and ending at with the number len(my_list) - 1, so that we have one index for each list element:

names = ['Tom', 'Yolanda', 'Estelle']
for index in range(len(names)):
    name = names[index]
    print(index, name)
0 Tom
1 Yolanda
2 Estelle

This is useful if we want to do something with the index, for instance, remember the position of elements matching a condition:

voltages = [1, 1, 10, 1, 1, 10, 1]

indices_of_the_voltage_peaks = []

for index in range(len(voltages)):
    voltage = voltages[index]
    if voltage > 4:
        indices_of_the_voltage_peaks.append(index)

print('The voltage exceeds 4 at these indices:', indices_of_the_voltage_peaks)
The voltage exceeds 4 at these indices: [2, 5]

While loops#

For loops are great if you have a pre-specified list of items and you want to do something with each item. Or if you have a computation and want to run it a fixed amount of times. However, sometimes you do not know beforehand how often you need to run a computation or you have an unspecified amount of items you need to work through.

So-called while loops allow you to apply a computation as long as a specified condition is met. While loops have the following form:

while CONDITION:
    do something

For instance, we can use a while loop to count down to zero:

count = 10

while count > 0:  # apply the indented code as long as the value of `count` is greater than 0
    print(count)
    count = count - 1

print(count)
10
9
8
7
6
5
4
3
2
1
0

Bonus content#

List comprehensions#

We very often encounter code that applies a function to list elements and appends the results ina new list using a for loop:

numbers = [123, 423, 540]

results = []
for number in numbers:
    results.append(number / 2)

List comprehensions are a way to write code like this more concisely:

results = [number / 2 for number in numbers]

Iterating over multiple lists in parallel using zip#

If we want to iterate over two lists in parallel, we can use indices produced by range to index into the different lists:

names = ['Tom', 'Ada', 'Jon']
ages = [14, 16, 12]

for index in range(len(names)):
    print(names[index], ages[index])
Tom 14
Ada 16
Jon 12

zip allows you to do that directly without having to use indices. Zip returns in each iteration a tuple of the individual list elemens:

names = ['Tom', 'Ada', 'Jon']
ages = [14, 16, 12]

for name, age in zip(names, ages):
    print(name, age)
Tom 14
Ada 16
Jon 12

Getting list indices and list elements in a for loop using enumerate#

If you need both the list indices and the elements, you can again use range:

names = ['Tom', 'Ada', 'Jon']

for index in range(len(names)):
    print(index, names[index])
0 Tom
1 Ada
2 Jon

enumerate returns can be more concise, since it returns a tuple of the index and the element in each iteration:

names = ['Tom', 'Ada', 'Jon']

for index, name in enumerate(names):
    print(index, name)
0 Tom
1 Ada
2 Jon

Spike detection using python#

  • Present data in code (individual voltage values, manipulate them and store the results) - variables

  • Compare variables (voltage to threshold) - boolean values

  • Perform different actions based on the value of a variable (only keep the position if the voltage exceeds the threshold) - if-else statements

  • Present and access data in a time series of voltage values - lists

  • Perform an action for each element in a sequence of values (inspect voltage values one-by-one) - for loops

Now you can write your spike detector!!

Next steps:

  • Separate data and logic so we can use the same code for new recordings - functions

  • Apply this to multi data files

  • Plot and save the results

  • Make everything more efficient and robust using numeric computation libraries (numpy, scipy)