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 40for 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 elementexecute 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)